Error('`interpolation_type` must be either "linear" or "nearest"')

    @property
    def opacity_unit_distance(self) -> float:
        """Return or set the opacity unit distance.

        This is the unit distance on which the scalar opacity transfer function
        is defined.

        By default this is 1.0, meaning that over a distance of 1.0 units, a
        given opacity (from the transfer function) is accumulated. This is
        adjusted for the actual sampling distance during rendering.
        """
        return self.GetScalarOpacityUnitDistance()

    @opacity_unit_distance.setter
    def opacity_unit_distance(self, value: float):
        self.SetScalarOpacityUnitDistance(value)

    @property
    def shade(self) -> bool:
        """Return or set shading of a volume.

        If shading is turned off, then the mapper for the volume will not
        perform shading calculations. If shading is turned on, the mapper may
        perform shading calculations - in some cases shading does not apply
        (for example, in a maximum intensity projection) and therefore shading
        will not be performed even if this flag is on. For a compositing type
        of mapper, turning shading off is generally the same as setting
        ``ambient=1``, ``diffuse=0``, ``specular=0``. Shading can be
        independently turned on/off per component.

        """
        return bool(self.GetShade())

    @shade.setter
    def shade(self, value: bool):
        self.SetShade(value)

    @property
    def independent_components(self) -> bool:
        """Return or set independent components.

        If ``False``, then you must have either 2 or 4 component data.
        For 2 component data, the first is passed through the
        first color transfer function and the second component is passed
        through the first scalar opacity (and gradient opacity) transfer
        function. Normals will be generated off of the second component. When
        using gradient based opacity modulation, the gradients are computed off
        of the second component.

        For 4 component data, the first three will directly represent RGB (no
        lookup table). The fourth component will be passed through the first
        scalar opacity transfer function for opacity and first gradient opacity
        transfer function for gradient based opacity modulation. Normals will
        be generated from the fourth component. When using gradient based
        opacity modulation, the gradients are computed off of the fourth
        component.
        """
        return bool(self.GetIndependentComponents())

    @independent_components.setter
    def independent_components(self, value: bool):
        self.SetIndependentComponents(value)

    @property
    def ambient(self) -> float:
        """Return or set ambient lighting coefficient.

        This is the amount of light in the range of 0 to 1 (default 0.0) that
        reaches the actor when not directed at the light source emitted from
        the viewer.

        Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
        set to ``True``.

        """
        return self.GetAmbient()

    @ambient.setter
    def ambient(self, value: float):
        self.SetAmbient(value)

    @property
    def diffuse(self) -> float:
        """Return or set the diffuse lighting coefficient.

        This is the scattering of light by reflection or transmission. Diffuse
        reflection results when light strikes an irregular surface such as a
        frosted window or the surface of a frosted or coated light bulb.

        Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
        set to ``True``.

        """
        return self.GetDiffuse()

    @diffuse.setter
    def diffuse(self, value: float):
        self.SetDiffuse(value)

    @property
    def specular(self) -> float:
        """Return or set specular.

        Default 0.0

        Specular lighting simulates the bright spot of a light that appears on
        shiny objects.

        Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
        set to ``True``.

        """
        return self.GetSpecular()

    @specular.setter
    def specular(self, value: float):
        self.SetSpecular(value)

    @property
    def specular_power(self) -> float:
        """Return or set specular power.

        The specular power. Between 0.0 and 128.0. Default 10.0

        """
        return self.GetSpecularPower()

    @specular_power.setter
    def specular_power(self, value: float):
        self.SetSpecularPower(value)

    def copy(self) -> 'VolumeProperty':
        """Create a deep copy of this property.

        Returns
        -------
        pyvista.plotting.volume_property.VolumeProperty
            Deep copy of this property.

        """
        new_prop = VolumeProperty()
        new_prop.DeepCopy(self)
        return new_prop

    def __repr__(self):
        """Representation of this property."""
        props = [
            f'{type(self).__name__} ({hex(id(self))})',
        ]

        for attr in dir(self):
            if not attr.startswith('_') and attr[0].islower():
                name = ' '.join(attr.split('_')).capitalize() + ':'
                value = getattr(self, attr)
                if callable(value):
                    continue
                if isinstance(value, str):
                    value = f'"{value}"'
                props.append(f'  {name:28s} {value}')

        return '\n'.join(props)


# pyvista/plotting/widgets.py

"""Module dedicated to widgets."""

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.utilities import (
    NORMALS,
    assert_empty_kwargs,
    generate_plane,
    get_array,
    get_array_association,
    try_callback,
)
from pyvista.utilities.algorithms import (
    add_ids_algorithm,
    algorithm_to_mesh_handler,
    crinkle_algorithm,
    outline_algorithm,
    pointset_to_polydata_algorithm,
    set_algorithm_input,
)

from .colors import Color


def _parse_interaction_event(interaction_event):
    """Parse the interaction event.

    Parameters
    ----------
    interaction_event : vtk.vtkCommand.EventIds, str, optional
        The VTK interaction event to use for triggering the callback. Accepts
        either the strings ``'start'``, ``'end'``, ``'always'`` or a
        ``vtk.vtkCommand.EventIds``.

    Returns
    -------
    vtk.vtkCommand.EventIds
        VTK Event type.

    """
    if interaction_event == 'start':
        interaction_event = _vtk.vtkCommand.StartInteractionEvent
    elif interaction_event == 'end':
        interaction_event = _vtk.vtkCommand.EndInteractionEvent
    elif interaction_event == 'always':
        interaction_event = _vtk.vtkCommand.InteractionEvent
    elif isinstance(interaction_event, str):
        raise ValueError(
            "Expected value for `interaction_event` is 'start', "
            f"'end', or 'always'. {interaction_event} was given."
        )
    elif not isinstance(interaction_event, _vtk.vtkCommand.EventIds):
        raise TypeError(
            "Expected type for `interaction_event` is either a str "
            "or an instance of `vtk.vtkCommand.EventIds`."
            f" ({type(interaction_event)}) was given."
        )
    return interaction_event


class WidgetHelper:
    """An internal class to manage widgets.

    It also manages and other helper methods involving widgets.

    """

    def __init__(self, *args, **kwargs):
        """Initialize widget helper."""
        super().__init__(*args, **kwargs)
        self.camera_widgets = []
        self.box_widgets = []
        self.box_clipped_meshes = []
        self.plane_widgets = []
        self.plane_clipped_meshes = []
        self.plane_sliced_meshes = []
        self.line_widgets = []
        self.slider_widgets = []
        self.threshold_meshes = []
        self.isovalue_meshes = []
        self.spline_widgets = []
        self.spline_sliced_meshes = []
        self.sphere_widgets = []
        self.button_widgets = []

    def add_box_widget(
        self,
        callback,
        bounds=None,
        factor=1.25,
        rotation_enabled=True,
        color=None,
        use_planes=False,
        outline_translation=True,
        pass_widget=False,
        interaction_event='end',
    ):
        """Add a box widget to the scene.

        This is useless without a callback function. You can pass a
        callable function that takes a single argument, the PolyData
        box output from this widget, and performs a task with that
        box.

        Parameters
        ----------
        callback : callable
            The method called every time the box is updated. This has
            two options: Take a single argument, the ``PolyData`` box
            (default) or if ``use_planes=True``, then it takes a
            single argument of the plane collection as a ``vtkPlanes``
            object.

        bounds : tuple(float)
            Length 6 tuple of the bounding box where the widget is
            placed.

        factor : float, optional
            An inflation factor to expand on the bounds when placing.

        rotation_enabled : bool, optional
            If ``False``, the box widget cannot be rotated and is
            strictly orthogonal to the Cartesian axes.

        color : ColorLike, optional
            Either a string, rgb sequence, or hex color string.
            Defaults to :attr:`pyvista.global_theme.font.color
            <pyvista.themes._Font.color>`.

        use_planes : bool, optional
            Changes the arguments passed to the callback to the planes
            that make up the box.

        outline_translation : bool, optional
            If ``False``, the box widget cannot be translated and is
            strictly placed at the given bounds.

        pass_widget : bool, optional
            If ``True``, the widget will be passed as the last
            argument of the callback.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Now accepts either strings or ``vtk.vtkCommand.EventIds``.

        Returns
        -------
        vtk.vtkBoxWidget
            Box widget.

        Examples
        --------
        Shows an interactive clip box.

        >>> import pyvista as pv
        >>> mesh = pv.ParametricConicSpiral()
        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh_clip_box(mesh, color='white')
        >>> pl.show()

        For a full example see :ref:`box_widget_example`.

        """
        interaction_event = _parse_interaction_event(interaction_event)

        if bounds is None:
            bounds = self.bounds

        def _the_callback(box_widget, event_id):
            the_box = pyvista.PolyData()
            box_widget.GetPolyData(the_box)
            planes = _vtk.vtkPlanes()
            box_widget.GetPlanes(planes)
            if callable(callback):
                if use_planes:
                    args = [planes]
                else:
                    args = [the_box]
                if pass_widget:
                    args.append(box_widget)
                try_callback(callback, *args)
            return

        box_widget = _vtk.vtkBoxWidget()
        box_widget.GetOutlineProperty().SetColor(
            Color(color, default_color=pyvista.global_theme.font.color).float_rgb
        )
        box_widget.SetInteractor(self.iren.interactor)
        box_widget.SetCurrentRenderer(self.renderer)
        box_widget.SetPlaceFactor(factor)
        box_widget.SetRotationEnabled(rotation_enabled)
        box_widget.SetTranslationEnabled(outline_translation)
        box_widget.PlaceWidget(bounds)
        box_widget.On()
        box_widget.AddObserver(interaction_event, _the_callback)
        _the_callback(box_widget, None)

        self.box_widgets.append(box_widget)
        return box_widget

    def clear_box_widgets(self):
        """Remove all of the box widgets."""
        self.box_widgets.clear()

    def add_mesh_clip_box(
        self,
        mesh,
        invert=False,
        rotation_enabled=True,
        widget_color=None,
        outline_translation=True,
        merge_points=True,
        crinkle=False,
        interaction_event='end',
        **kwargs,
    ):
        """Clip a mesh using a box widget.

        Add a mesh to the scene with a box widget that is used to clip
        the mesh interactively.

        The clipped mesh is saved to the ``.box_clipped_meshes`` attribute on
        the plotter.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and clip or algorithm that
            produces said mesh.

        invert : bool, optional
            Flag on whether to flip/invert the clip.

        rotation_enabled : bool, optional
            If ``False``, the box widget cannot be rotated and is strictly
            orthogonal to the cartesian axes.

        widget_color : ColorLike, optional
            Color of the widget.  Either a string, RGB sequence, or
            hex color string.  For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        outline_translation : bool, optional
            If ``False``, the plane widget cannot be translated and is
            strictly placed at the given bounds.

        merge_points : bool, optional
            If ``True`` (default), coinciding points of independently
            defined mesh elements will be merged.

        crinkle : bool, optional
            Crinkle the clip by extracting the entire cells along the clip.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Changed from ``event_type`` to ``interaction_event`` and now
               accepts either strings and ``vtk.vtkCommand.EventIds``.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        from pyvista.core.filters import _get_output  # avoids circular import

        mesh, algo = algorithm_to_mesh_handler(
            add_ids_algorithm(mesh, point_ids=False, cell_ids=True)
        )

        name = kwargs.get('name', mesh.memory_address)
        rng = mesh.get_data_range(kwargs.get('scalars', None))
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        mesh.set_active_scalars(kwargs.get('scalars', mesh.active_scalars_name))

        self.add_mesh(outline_algorithm(algo), name=f"{name}-outline", opacity=0.0)

        port = 1 if invert else 0

        clipper = _vtk.vtkBoxClipDataSet()
        if not merge_points:
            # vtkBoxClipDataSet uses vtkMergePoints by default
            clipper.SetLocator(_vtk.vtkNonMergingPointLocator())
        set_algorithm_input(clipper, algo)
        clipper.GenerateClippedOutputOn()

        if crinkle:
            crinkler = crinkle_algorithm(clipper.GetOutputPort(port), algo)
            box_clipped_mesh = _get_output(crinkler)
        else:
            box_clipped_mesh = _get_output(clipper, oport=port)

        self.box_clipped_meshes.append(box_clipped_mesh)

        def callback(planes):
            bounds = []
            for i in range(planes.GetNumberOfPlanes()):
                plane = planes.GetPlane(i)
                bounds.append(plane.GetNormal())
                bounds.append(plane.GetOrigin())

            clipper.SetBoxClip(*bounds)
            clipper.Update()
            if crinkle:
                clipped = pyvista.wrap(crinkler.GetOutputDataObject(0))
            else:
                clipped = _get_output(clipper, oport=port)
            box_clipped_mesh.shallow_copy(clipped)

        self.add_box_widget(
            callback=callback,
            bounds=mesh.bounds,
            factor=1.25,
            rotation_enabled=rotation_enabled,
            use_planes=True,
            color=widget_color,
            outline_translation=outline_translation,
            interaction_event=interaction_event,
        )

        if crinkle:
            return self.add_mesh(crinkler, reset_camera=False, **kwargs)
        return self.add_mesh(clipper.GetOutputPort(port), reset_camera=False, **kwargs)

    def add_plane_widget(
        self,
        callback,
        normal='x',
        origin=None,
        bounds=None,
        factor=1.25,
        color=None,
        assign_to_axis=None,
        tubing=False,
        outline_translation=False,
        origin_translation=True,
        implicit=True,
        pass_widget=False,
        test_callback=True,
        normal_rotation=True,
        interaction_event='end',
    ):
        """Add a plane widget to the scene.

        This is useless without a callback function. You can pass a
        callable function that takes two arguments, the normal and
        origin of the plane in that order output from this widget, and
        performs a task with that plane.

        Parameters
        ----------
        callback : callable
            The method called every time the plane is updated. Takes
            two arguments, the normal and origin of the plane in that
            order.

        normal : str or tuple(float)
            The starting normal vector of the plane.

        origin : tuple(float)
            The starting coordinate of the center of the plane.

        bounds : tuple(float)
            Length 6 tuple of the bounding box where the widget is placed.

        factor : float, optional
            An inflation factor to expand on the bounds when placing.

        color : ColorLike, optional
            Either a string, rgb list, or hex color string.

        assign_to_axis : str or int, optional
            Assign the normal of the plane to be parallel with a given
            axis: options are ``(0, 'x')``, ``(1, 'y')``, or ``(2,
            'z')``.

        tubing : bool, optional
            When using an implicit plane wiget, this controls whether
            or not tubing is shown around the plane's boundaries.

        outline_translation : bool, optional
            If ``False``, the plane widget cannot be translated and is
            strictly placed at the given bounds. Only valid when using
            an implicit plane.

        origin_translation : bool, optional
            If ``False``, the plane widget cannot be translated by its
            origin and is strictly placed at the given origin. Only
            valid when using an implicit plane.

        implicit : bool, optional
            When ``True``, a ``vtkImplicitPlaneWidget`` is used and
            when ``False``, a ``vtkPlaneWidget`` is used.

        pass_widget : bool, optional
            If ``True``, the widget will be passed as the last
            argument of the callback.

        test_callback : bool, optional
            If ``True``, run the callback function after the widget is
            created.

        normal_rotation : bool, optional
            Set the opacity of the normal vector arrow to 0 such that
            it is effectively disabled. This prevents the user from
            rotating the normal. This is forced to ``False`` when
            ``assign_to_axis`` is set.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Now accepts either strings and ``vtk.vtkCommand.EventIds``.

        Returns
        -------
        vtk.vtkImplicitPlaneWidget or vtk.vtkPlaneWidget
            Plane widget.

        """
        interaction_event = _parse_interaction_event(interaction_event)

        if origin is None:
            origin = self.center
        if bounds is None:
            bounds = self.bounds

        if isinstance(normal, str):
            normal = NORMALS[normal.lower()]

        color = Color(color, default_color=pyvista.global_theme.font.color)

        if assign_to_axis:
            normal_rotation = False

        def _the_callback(widget, event_id):
            the_plane = _vtk.vtkPlane()
            widget.GetPlane(the_plane)
            normal = the_plane.GetNormal()
            origin = the_plane.GetOrigin()
            if callable(callback):
                if pass_widget:
                    try_callback(callback, normal, origin, widget)
                else:
                    try_callback(callback, normal, origin)
            return

        if implicit:
            plane_widget = _vtk.vtkImplicitPlaneWidget()
            plane_widget.GetNormalProperty().SetColor(color.float_rgb)
            plane_widget.GetOutlineProperty().SetColor(color.float_rgb)
            plane_widget.GetOutlineProperty().SetColor(color.float_rgb)
            plane_widget.GetOutlineProperty().SetOpacity(color.opacity)
            plane_widget.SetTubing(tubing)
            plane_widget.SetOutlineTranslation(outline_translation)
            plane_widget.SetOriginTranslation(origin_translation)

            _start_interact = lambda plane_widget, event: plane_widget.SetDrawPlane(True)
            _stop_interact = lambda plane_widget, event: plane_widget.SetDrawPlane(False)

            plane_widget.SetDrawPlane(False)
            plane_widget.AddObserver(_vtk.vtkCommand.StartInteractionEvent, _start_interact)
            plane_widget.AddObserver(_vtk.vtkCommand.EndInteractionEvent, _stop_interact)
            plane_widget.SetPlaceFactor(factor)
            plane_widget.PlaceWidget(bounds)
            plane_widget.SetOrigin(origin)

            if not normal_rotation:
                plane_widget.GetNormalProperty().SetOpacity(0)

        else:
            # Position of the small plane
            source = _vtk.vtkPlaneSource()
            source.SetNormal(normal)
            source.SetCenter(origin)
            source.SetPoint1(
                origin[0] + (bounds[1] - bounds[0]) * 0.01,
                origin[1] - (bounds[3] - bounds[2]) * 0.01,
                origin[2],
            )
            source.SetPoint2(
                origin[0] - (bounds[1] - bounds[0]) * 0.01,
                origin[1] + (bounds[3] - bounds[2]) * 0.01,
                origin[2],
            )
            source.Update()
            plane_widget = _vtk.vtkPlaneWidget()
            plane_widget.SetHandleSize(0.01)
            # Position of the widget
            plane_widget.SetInputData(source.GetOutput())
            plane_widget.SetRepresentationToOutline()
            plane_widget.SetPlaceFactor(factor)
            plane_widget.PlaceWidget(bounds)
            plane_widget.SetCenter(origin)  # Necessary
            plane_widget.GetPlaneProperty().SetColor(color.float_rgb)  # self.C_LOT[fn])
            plane_widget.GetHandleProperty().SetColor(color.float_rgb)

            if not normal_rotation:
                plane_widget.GetHandleProperty().SetOpacity(0)

        plane_widget.GetPlaneProperty().SetOpacity(0.5)
        plane_widget.SetInteractor(self.iren.interactor)
        plane_widget.SetCurrentRenderer(self.renderer)

        if assign_to_axis:
            # Note that normal_rotation was forced to False
            if assign_to_axis in [0, "x", "X"]:
                plane_widget.NormalToXAxisOn()
                plane_widget.SetNormal(NORMALS["x"])
            elif assign_to_axis in [1, "y", "Y"]:
                plane_widget.NormalToYAxisOn()
                plane_widget.SetNormal(NORMALS["y"])
            elif assign_to_axis in [2, "z", "Z"]:
                plane_widget.NormalToZAxisOn()
                plane_widget.SetNormal(NORMALS["z"])
            else:
                raise RuntimeError("assign_to_axis not understood")
        else:
            plane_widget.SetNormal(normal)

        plane_widget.Modified()
        plane_widget.UpdatePlacement()
        plane_widget.On()
        plane_widget.AddObserver(
            interaction_event,
            # _vtk.vtkCommand.InteractionEvent,
            _the_callback,
        )
        if test_callback:
            _the_callback(plane_widget, None)  # Trigger immediate update

        self.plane_widgets.append(plane_widget)
        return plane_widget

    def clear_plane_widgets(self):
        """Remove all of the plane widgets."""
        self.plane_widgets.clear()

    def add_mesh_clip_plane(
        self,
        mesh,
        normal='x',
        invert=False,
        widget_color=None,
        value=0.0,
        assign_to_axis=None,
        tubing=False,
        origin_translation=True,
        outline_translation=False,
        implicit=True,
        normal_rotation=True,
        crinkle=False,
        interaction_event='end',
        origin=None,
        **kwargs,
    ):
        """Clip a mesh using a plane widget.

        Add a mesh to the scene with a plane widget that is used to clip
        the mesh interactively.

        The clipped mesh is saved to the ``.plane_clipped_meshes``
        attribute on the plotter.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and clip or algorithm that
            produces said mesh.

        normal : str or tuple(float), optional
            The starting normal vector of the plane.

        invert : bool, optional
            Flag on whether to flip/invert the clip.

        widget_color : ColorLike, optional
            Either a string, RGB list, or hex color string.

        value : float, optional
            Set the clipping value along the normal direction.
            The default value is 0.0.

        assign_to_axis : str or int, optional
            Assign the normal of the plane to be parallel with a given
            axis.  Options are ``(0, 'x')``, ``(1, 'y')``, or ``(2,
            'z')``.

        tubing : bool, optional
            When using an implicit plane wiget, this controls whether
            or not tubing is shown around the plane's boundaries.

        origin_translation : bool, optional
            If ``False``, the plane widget cannot be translated by its
            origin and is strictly placed at the given origin. Only
            valid when using an implicit plane.

        outline_translation : bool, optional
            If ``False``, the box widget cannot be translated and is
            strictly placed at the given bounds.

        implicit : bool, optional
            When ``True``, a ``vtkImplicitPlaneWidget`` is used and
            when ``False``, a ``vtkPlaneWidget`` is used.

        normal_rotation : bool, optional
            Set the opacity of the normal vector arrow to 0 such that
            it is effectively disabled. This prevents the user from
            rotating the normal. This is forced to ``False`` when
            ``assign_to_axis`` is set.

        crinkle : bool, optional
            Crinkle the clip by extracting the entire cells along the clip.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Now accepts either strings or ``vtk.vtkCommand.EventIds``.

        origin : tuple(float), optional
            The starting coordinate of the center of the plane.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        from pyvista.core.filters import _get_output  # avoids circular import

        mesh, algo = algorithm_to_mesh_handler(
            add_ids_algorithm(mesh, point_ids=False, cell_ids=True)
        )

        name = kwargs.get('name', mesh.memory_address)
        rng = mesh.get_data_range(kwargs.get('scalars', None))
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        mesh.set_active_scalars(kwargs.get('scalars', mesh.active_scalars_name))
        if origin is None:
            origin = mesh.center

        self.add_mesh(outline_algorithm(algo), name=f"{name}-outline", opacity=0.0)

        if isinstance(mesh, _vtk.vtkPolyData):
            clipper = _vtk.vtkClipPolyData()
        # elif isinstance(mesh, vtk.vtkImageData):
        #     clipper = vtk.vtkClipVolume()
        #     clipper.SetMixed3DCellGeneration(True)
        else:
            clipper = _vtk.vtkTableBasedClipDataSet()
        set_algorithm_input(clipper, algo)
        clipper.SetValue(value)
        clipper.SetInsideOut(invert)  # invert the clip if needed

        if crinkle:
            crinkler = crinkle_algorithm(clipper, algo)
            plane_clipped_mesh = _get_output(crinkler)
        else:
            plane_clipped_mesh = _get_output(clipper)
        self.plane_clipped_meshes.append(plane_clipped_mesh)

        def callback(normal, loc):
            function = generate_plane(normal, loc)
            clipper.SetClipFunction(function)  # the implicit function
            clipper.Update()  # Perform the Cut
            if crinkle:
                clipped = pyvista.wrap(crinkler.GetOutputDataObject(0))
            else:
                clipped = pyvista.wrap(clipper.GetOutput())
            plane_clipped_mesh.shallow_copy(clipped)

        self.add_plane_widget(
            callback=callback,
            bounds=mesh.bounds,
            factor=1.25,
            normal=normal,
            color=widget_color,
            tubing=tubing,
            assign_to_axis=assign_to_axis,
            origin_translation=origin_translation,
            outline_translation=outline_translation,
            implicit=implicit,
            origin=origin,
            normal_rotation=normal_rotation,
            interaction_event=interaction_event,
        )

        if crinkle:
            return self.add_mesh(crinkler, **kwargs)
        return self.add_mesh(clipper, **kwargs)

    def add_volume_clip_plane(
        self,
        volume,
        normal='x',
        invert=False,
        widget_color=None,
        value=0.0,
        assign_to_axis=None,
        tubing=False,
        origin_translation=True,
        outline_translation=False,
        implicit=True,
        normal_rotation=True,
        interaction_event='end',
        origin=None,
        **kwargs,
    ):
        """Clip a volume using a plane widget.

        Parameters
        ----------
        volume : pyvista.plotting.volume.Volume or pyvista.UniformGrid or pyvista.RectilinearGrid
            New dataset of type :class:`pyvista.UniformGrid` or
            :class:`pyvista.RectilinearGrid`, or the return value from
            :class:`pyvista.plotting.volume.Volume` from :func:`Plotter.add_volume`.

        normal : str or tuple(float), optional
            The starting normal vector of the plane.

        invert : bool, optional
            Flag on whether to flip/invert the clip.

        widget_color : ColorLike, optional
            Either a string, RGB list, or hex color string.

        value : float, optional
            Set the clipping value along the normal direction.
            The default value is 0.0.

        assign_to_axis : str or int, optional
            Assign the normal of the plane to be parallel with a given
            axis.  Options are ``(0, 'x')``, ``(1, 'y')``, or ``(2,
            'z')``.

        tubing : bool, optional
            When using an implicit plane wiget, this controls whether
            or not tubing is shown around the plane's boundaries.

        origin_translation : bool, optional
            If ``False``, the plane widget cannot be translated by its
            origin and is strictly placed at the given origin. Only
            valid when using an implicit plane.

        outline_translation : bool, optional
            If ``False``, the box widget cannot be translated and is
            strictly placed at the given bounds.

        implicit : bool, optional
            When ``True``, a ``vtkImplicitPlaneWidget`` is used and
            when ``False``, a ``vtkPlaneWidget`` is used.

        normal_rotation : bool, optional
            Set the opacity of the normal vector arrow to 0 such that
            it is effectively disabled. This prevents the user from
            rotating the normal. This is forced to ``False`` when
            ``assign_to_axis`` is set.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        origin : tuple(float), optional
            The starting coordinate of the center of the plane.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_volume` to control how the volume is
            displayed. Only applicable if ``volume`` is either a
            :class:`pyvista.UniformGrid` and :class:`pyvista.RectilinearGrid`.

        Returns
        -------
        vtk.vtkPlaneWidget or vtk.vtkImplicitPlaneWidget
            The VTK plane widget depending on the value of ``implicit``.

        """
        if isinstance(volume, (pyvista.UniformGrid, pyvista.RectilinearGrid)):
            volume = self.add_volume(volume, **kwargs)
        elif not isinstance(volume, pyvista.plotting.Volume):
            raise TypeError(
                'The `volume` parameter type must be either pyvista.UniformGrid, '
                'pyvista.RectilinearGrid, or a pyvista.plotting.volume.Volume '
                'from `Plotter.add_volume`.'
            )
        else:
            assert_empty_kwargs(**kwargs)

        plane = _vtk.vtkPlane()

        def callback(normal, origin):
            """Update the plane used to clip the volume."""
            plane.SetNormal(normal)
            plane.SetOrigin(origin)

        widget = self.add_plane_widget(
            callback=callback,
            bounds=volume.bounds,
            factor=1.25,
            normal=normal,
            color=widget_color,
            tubing=tubing,
            assign_to_axis=assign_to_axis,
            origin_translation=origin_translation,
            outline_translation=outline_translation,
            implicit=implicit,
            origin=origin,
            normal_rotation=normal_rotation,
            interaction_event=interaction_event,
        )
        widget.GetPlane(plane)
        volume.mapper.AddClippingPlane(plane)
        self.plane_widgets.append(widget)

        return widget

    def add_mesh_slice(
        self,
        mesh,
        normal='x',
        generate_triangles=False,
        widget_color=None,
        assign_to_axis=None,
        tubing=False,
        origin_translation=True,
        outline_translation=False,
        implicit=True,
        normal_rotation=True,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
        origin=None,
        **kwargs,
    ):
        """Slice a mesh using a plane widget.

        Add a mesh to the scene with a plane widget that is used to slice
        the mesh interactively.

        The sliced mesh is saved to the ``.plane_sliced_meshes`` attribute on
        the plotter.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and slice or algorithm that
            produces said mesh.

        normal : str or tuple(float), optional
            The starting normal vector of the plane.

        generate_triangles : bool, optional
            If this is enabled (``False`` by default), the output will be
            triangles otherwise, the output will be the intersection polygons.

        widget_color : ColorLike, optional
            Either a string, RGB sequence, or hex color string.  Defaults
            to ``'white'``.

        assign_to_axis : str or int, optional
            Assign the normal of the plane to be parallel with a given axis:
            options are (0, 'x'), (1, 'y'), or (2, 'z').

        tubing : bool, optional
            When using an implicit plane wiget, this controls whether or not
            tubing is shown around the plane's boundaries.

        origin_translation : bool, optional
            If ``False``, the plane widget cannot be translated by its origin
            and is strictly placed at the given origin. Only valid when using
            an implicit plane.

        outline_translation : bool, optional
            If ``False``, the box widget cannot be translated and is strictly
            placed at the given bounds.

        implicit : bool, optional
            When ``True``, a ``vtkImplicitPlaneWidget`` is used and when
            ``False``, a ``vtkPlaneWidget`` is used.

        normal_rotation : bool, optional
            Set the opacity of the normal vector arrow to 0 such that it is
            effectively disabled. This prevents the user from rotating the
            normal. This is forced to ``False`` when ``assign_to_axis`` is set.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        origin : tuple(float), optional
            The starting coordinate of the center of the plane.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        mesh, algo = algorithm_to_mesh_handler(mesh)

        name = kwargs.get('name', mesh.memory_address)
        rng = mesh.get_data_range(kwargs.get('scalars', None))
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        mesh.set_active_scalars(kwargs.get('scalars', mesh.active_scalars_name))
        if origin is None:
            origin = mesh.center

        self.add_mesh(outline_algorithm(algo or mesh), name=f"{name}-outline", opacity=0.0)

        alg = _vtk.vtkCutter()  # Construct the cutter object
        set_algorithm_input(alg, algo or mesh)
        if not generate_triangles:
            alg.GenerateTrianglesOff()

        plane_sliced_mesh = pyvista.wrap(alg.GetOutput())
        self.plane_sliced_meshes.append(plane_sliced_mesh)

        def callback(normal, origin):
            # create the plane for clipping
            plane = generate_plane(normal, origin)
            alg.SetCutFunction(plane)  # the cutter to use the plane we made
            alg.Update()  # Perform the Cut
            plane_sliced_mesh.shallow_copy(alg.GetOutput())

        self.add_plane_widget(
            callback=callback,
            bounds=mesh.bounds,
            factor=1.25,
            normal=normal,
            color=widget_color,
            tubing=tubing,
            assign_to_axis=assign_to_axis,
            origin_translation=origin_translation,
            outline_translation=outline_translation,
            implicit=implicit,
            origin=origin,
            normal_rotation=normal_rotation,
            interaction_event=interaction_event,
        )

        return self.add_mesh(alg, **kwargs)

    def add_mesh_slice_orthogonal(
        self,
        mesh,
        generate_triangles=False,
        widget_color=None,
        tubing=False,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
        **kwargs,
    ):
        """Slice a mesh with three interactive planes.

        Adds three interactive plane slicing widgets for orthogonal slicing
        along each cartesian axis.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and threshold or algorithm
            that produces said mesh.

        generate_triangles : bool, optional
            If this is enabled (``False`` by default), the output will be
            triangles otherwise, the output will be the intersection polygons.

        widget_color : ColorLike, optional
            Color of the widget.  Either a string, RGB sequence, or
            hex color string.  For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        tubing : bool, optional
            When using an implicit plane wiget, this controls whether or not
            tubing is shown around the plane's boundaries.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        list
            List of vtk.vtkActor(s).

        """
        actors = []
        name = kwargs.pop("name", None)
        for ax in ["x", "y", "z"]:
            axkwargs = kwargs.copy()
            if name:
                axkwargs["name"] = f"{name}-{ax}"
            a = self.add_mesh_slice(
                mesh,
                assign_to_axis=ax,
                origin_translation=False,
                outline_translation=False,
                generate_triangles=generate_triangles,
                widget_color=widget_color,
                tubing=tubing,
                interaction_event=interaction_event,
                **axkwargs,
            )
            actors.append(a)

        return actors

    def add_line_widget(
        self,
        callback,
        bounds=None,
        factor=1.25,
        resolution=100,
        color=None,
        use_vertices=False,
        pass_widget=False,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
    ):
        """Add a line widget to the scene.

        This is useless without a callback function. You can pass a
        callable function that takes a single argument, the PolyData
        line output from this widget, and performs a task with that
        line.

        Parameters
        ----------
        callback : callable
            The method called every time the line is updated. This has
            two options: Take a single argument, the ``PolyData`` line
            (default) or if ``use_vertices=True``, then it can take
            two arguments of the coordinates of the line's end points.

        bounds : tuple(float), optional
            Length 6 tuple of the bounding box where the widget is
            placed.

        factor : float, optional
            An inflation factor to expand on the bounds when placing.

        resolution : int, optional
            The number of points in the line created.

        color : ColorLike, optional
            Either a string, rgb sequence, or hex color string.

        use_vertices : bool, optional
            Changes the arguments of the callback method to take the end
            points of the line instead of a PolyData object.

        pass_widget : bool, default: False
            If ``True``, the widget will be passed as the last
            argument of the callback.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        Returns
        -------
        vtk.vtkLineWidget
            Created line widget.

        """
        if bounds is None:
            bounds = self.bounds

        color = Color(color, default_color=pyvista.global_theme.font.color)

        def _the_callback(widget, event_id):
            pointa = widget.GetPoint1()
            pointb = widget.GetPoint2()
            if callable(callback):
                if use_vertices:
                    args = [pointa, pointb]
                else:
                    the_line = pyvista.Line(pointa, pointb, resolution=resolution)
                    args = [the_line]
                if pass_widget:
                    args.append(widget)
                try_callback(callback, *args)

        line_widget = _vtk.vtkLineWidget()
        line_widget.GetLineProperty().SetColor(color.float_rgb)
        line_widget.SetInteractor(self.iren.interactor)
        line_widget.SetCurrentRenderer(self.renderer)
        line_widget.SetPlaceFactor(factor)
        line_widget.PlaceWidget(bounds)
        line_widget.SetResolution(resolution)
        line_widget.Modified()
        line_widget.On()
        line_widget.AddObserver(interaction_event, _the_callback)
        _the_callback(line_widget, None)

        self.line_widgets.append(line_widget)
        return line_widget

    def clear_line_widgets(self):
        """Remove all of the line widgets."""
        self.line_widgets.clear()

    def add_text_slider_widget(
        self,
        callback,
        data,
        value=None,
        pointa=(0.4, 0.9),
        pointb=(0.9, 0.9),
        color=None,
        interaction_event='end',
        style=None,
        **kwargs,
    ):
        """Add a text slider bar widget.

        This is useless without a callback function. You can pass a callable
        function that takes a single argument, the value of this slider widget,
        and performs a task with that value.

        Parameters
        ----------
        callback : callable
            The method called every time the slider is updated. This should take
            a single parameter: the float value of the slider.

        data : list
            The list of possible values displayed on the slider bar.

        value : float, optional
            The starting value of the slider.

        pointa : tuple(float), optional
            The relative coordinates of the left point of the slider on the
            display port.

        pointb : tuple(float), optional
            The relative coordinates of the right point of the slider on the
            display port.

        color : ColorLike, optional
            Either a string, RGB list, or hex color string.  Defaults
            to :attr:`pyvista.global_theme.font.color
            <pyvista.themes._Font.color>`.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Changed from ``event_type`` to ``interaction_event`` and now
               accepts either strings or ``vtk.vtkCommand.EventIds``.

        style : str, optional
            The name of the slider style. The list of available styles
            are in ``pyvista.global_theme.slider_styles``. Defaults to
            ``None``.

        **kwargs : dict, optional
            Deprecated keyword arguments.

            .. deprecated:: 0.38.0
               Keyword argument ``event_type`` deprecated in favor of
               ``interaction_event``.

        Returns
        -------
        vtk.vtkSliderWidget
            The VTK slider widget configured to display text.

        """
        if not isinstance(data, list):
            raise TypeError(
                f"The `data` parameter must be a list but {type(data).__name__} was passed instead"
            )
        n_states = len(data)
        if n_states == 0:
            raise ValueError("The input list of values is empty")
        delta = (n_states - 1) / float(n_states)
        # avoid division by zero in case there is only one element
        delta = 1 if delta == 0 else delta

        def _the_callback(value):
            if isinstance(value, float):
                idx = int(value / delta)
                # handle limit index
                if idx == n_states:
                    idx = n_states - 1
                if callable(callback):
                    try_callback(callback, data[idx])
            return

        slider_widget = self.add_slider_widget(
            callback=_the_callback,
            rng=[0, n_states - 1],
            value=value,
            pointa=pointa,
            pointb=pointb,
            color=color,
            interaction_event=interaction_event,
            style=style,
        )
        slider_rep = slider_widget.GetRepresentation()
        slider_rep.ShowSliderLabelOff()

        def title_callback(widget, event):
            value = widget.GetRepresentation().GetValue()
            idx = int(value / delta)
            # handle limit index
            if idx == n_states:
                idx = n_states - 1
            slider_rep.SetTitleText(data[idx])

        slider_widget.AddObserver(_parse_interaction_event(interaction_event), title_callback)
        title_callback(slider_widget, None)
        return slider_widget

    def add_slider_widget(
        self,
        callback,
        rng,
        value=None,
        title=None,
        pointa=(0.4, 0.9),
        pointb=(0.9, 0.9),
        color=None,
        pass_widget=False,
        interaction_event='end',
        style=None,
        title_height=0.03,
        title_opacity=1.0,
        title_color=None,
        fmt=None,
        slider_width=None,
        tube_width=None,
        **kwargs,
    ):
        """Add a slider bar widget.

        This is useless without a callback function. You can pass a
        callable function that takes a single argument, the value of
        this slider widget, and performs a task with that value.

        Parameters
        ----------
        callback : callable
            Called every time the slider is updated. This should take a single
            parameter: the float value of the slider. If ``pass_widget=True``,
            callable should take two parameters: the float value of the slider
            and the widget itself.

        rng : tuple(float)
            Length two tuple of the minimum and maximum ranges of the
            slider.

        value : float, optional
            The starting value of the slider.

        title : str, optional
            The string label of the slider widget.

        pointa : tuple(float), optional
            The relative coordinates of the left point of the slider
            on the display port.

        pointb : tuple(float), optional
            The relative coordinates of the right point of the slider
            on the display port.

        color : ColorLike, optional
            Either a string, RGB list, or hex color string.  Defaults
            to :attr:`pyvista.global_theme.font.color
            <pyvista.themes._Font.color>`.

        pass_widget : bool, optional
            If ``True``, the widget will be passed as the last
            argument of the callback.

        interaction_event : vtk.vtkCommand.EventIds, str, optional
            The VTK interaction event to use for triggering the
            callback. Accepts either the strings ``'start'``, ``'end'``,
            ``'always'`` or a ``vtk.vtkCommand.EventIds``.

            .. versionchanged:: 0.38.0
               Changed from ``event_type`` to ``interaction_event`` and now accepts
               either strings or ``vtk.vtkCommand.EventIds``.

        style : str, optional
            The name of the slider style. The list of available styles
            are in ``pyvista.global_theme.slider_styles``. Defaults to
            ``None``.

        title_height : float, optional
            Relative height of the title as compared to the length of
            the slider.

        title_opacity : float, optional
            Opacity of title. Defaults to 1.0.

        title_color : ColorLike, optional
            Either a string, RGB sequence, or hex color string.  Defaults
            to the value given in ``color``.

        fmt : str, optional
            String formatter used to format numerical data. Defaults
            to ``None``.

        slider_width : float, optional
            Normalized width of the slider. Defaults to the theme's slider width.

        tube_width : float, optional
            Normalized width of the tube. Defaults to the theme's tube width.

        **kwargs : dict, optional
            Deprecated keyword arguments.

            .. deprecated:: 0.38.0
               Keyword argument ``event_type`` deprecated in favor of
               ``interaction_event``.

        Returns
        -------
        vtk.vtkSliderWidget
            Slider widget.

        Examples
        --------
        >>> import pyvista as pv
        >>> pl = pv.Plotter()
        >>> def create_mesh(value):
        ...     res = int(value)
        ...     sphere = pv.Sphere(
        ...         phi_resolution=res, theta_resolution=res
        ...     )
        ...     pl.add_mesh(sphere, name="sphere", show_edges=True)
        ...
        >>> slider = pl.add_slider_widget(
        ...     create_mesh,
        ...     [5, 100],
        ...     title="Resolution",
        ...     title_opacity=0.5,
        ...     title_color="red",
        ...     fmt="%0.9f",
        ...     title_height=0.08,
        ... )
        >>> pl.show()
        """
        if self.iren is None:
            raise RuntimeError('Cannot add a widget to a closed plotter.')

        if value is None:
            value = ((rng[1] - rng[0]) / 2) + rng[0]

        color = Color(color, default_color=pyvista.global_theme.font.color)
        title_color = Color(title_color, default_color=color)

        if fmt is None:
            fmt = pyvista.global_theme.font.fmt

        def normalize(point, viewport):
            return (point[0] * (viewport[2] - viewport[0]), point[1] * (viewport[3] - viewport[1]))

        pointa = normalize(pointa, self.renderer.GetViewport())
        pointb = normalize(pointb, self.renderer.GetViewport())

        slider_rep = _vtk.vtkSliderRepresentation2D()
        slider_rep.SetPickable(False)
        slider_rep.SetMinimumValue(rng[0])
        slider_rep.SetMaximumValue(rng[1])
        slider_rep.SetValue(value)
        slider_rep.SetTitleText(title)
        slider_rep.GetTitleProperty().SetColor(color.float_rgb)
        slider_rep.GetSliderProperty().SetColor(color.float_rgb)
        slider_rep.GetCapProperty().SetColor(color.float_rgb)
        slider_rep.GetLabelProperty().SetColor(color.float_rgb)
        slider_rep.GetTubeProperty().SetColor(color.float_rgb)
        slider_rep.GetPoint1Coordinate().SetCoordinateSystemToNormalizedDisplay()
        slider_rep.GetPoint1Coordinate().SetValue(pointa[0], pointa[1])
        slider_rep.GetPoint2Coordinate().SetCoordinateSystemToNormalizedDisplay()
        slider_rep.GetPoint2Coordinate().SetValue(pointb[0], pointb[1])
        slider_rep.SetSliderLength(0.05)
        slider_rep.SetSliderWidth(0.05)
        slider_rep.SetEndCapLength(0.01)

        if style is not None:
            if not isinstance(style, str):
                raise TypeError(
                    f"Expected type for ``style`` is str but {type(style).__name__} was given."
                )
            slider_style = getattr(pyvista.global_theme.slider_styles, style)
            slider_rep.SetSliderLength(slider_style.slider_length)
            slider_rep.SetSliderWidth(slider_style.slider_width)
            slider_rep.GetSliderProperty().SetColor(slider_style.slider_color.float_rgb)
            slider_rep.SetTubeWidth(slider_style.tube_width)
            slider_rep.GetTubeProperty().SetColor(slider_style.tube_color.float_rgb)
            slider_rep.GetCapProperty().SetOpacity(slider_style.cap_opacity)
            slider_rep.SetEndCapLength(slider_style.cap_length)
            slider_rep.SetEndCapWidth(slider_style.cap_width)

        if slider_width is not None:
            slider_rep.SetSliderWidth(slider_width)
        if tube_width is not None:
            slider_rep.SetTubeWidth(tube_width)

        def _the_callback(widget, event):
            value = widget.GetRepresentation().GetValue()
            if callable(callback):
                if pass_widget:
                    try_callback(callback, value, widget)
                else:
                    try_callback(callback, value)
            return

        slider_widget = _vtk.vtkSliderWidget()
        slider_widget.SetInteractor(self.iren.interactor)
        slider_widget.SetCurrentRenderer(self.renderer)
        slider_widget.SetRepresentation(slider_rep)
        slider_widget.GetRepresentation().SetTitleHeight(title_height)
        slider_widget.GetRepresentation().GetTitleProperty().SetOpacity(title_opacity)
        slider_widget.GetRepresentation().GetTitleProperty().SetColor(title_color.float_rgb)
        if fmt is not None:
            slider_widget.GetRepresentation().SetLabelFormat(fmt)
        slider_widget.On()
        slider_widget.AddObserver(_parse_interaction_event(interaction_event), _the_callback)
        _the_callback(slider_widget, None)

        self.slider_widgets.append(slider_widget)
        return slider_widget

    def clear_slider_widgets(self):
        """Remove all of the slider widgets."""
        self.slider_widgets.clear()

    def add_mesh_threshold(
        self,
        mesh,
        scalars=None,
        invert=False,
        widget_color=None,
        preference='cell',
        title=None,
        pointa=(0.4, 0.9),
        pointb=(0.9, 0.9),
        continuous=False,
        all_scalars=False,
        method='upper',
        **kwargs,
    ):
        """Apply a threshold on a mesh with a slider.

        Add a mesh to the scene with a slider widget that is used to
        threshold the mesh interactively.

        The threshold mesh is saved to the ``.threshold_meshes`` attribute on
        the plotter.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and threshold or algorithm
            that produces said mesh.

        scalars : str, optional
            The string name of the scalars on the mesh to threshold and display.

        invert : bool, default: False
            Invert the threshold results. That is, cells that would have been
            in the output with this option off are excluded, while cells that
            would have been excluded from the output are included.

        widget_color : ColorLike, optional
            Color of the widget.  Either a string, RGB sequence, or
            hex color string.  For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        preference : str, default: 'cell'
            When ``mesh.n_points == mesh.n_cells`` and setting
            scalars, this parameter sets how the scalars will be
            mapped to the mesh.  Default ``'cell'``, causes the
            scalars to be associated with the mesh cells.  Can be
            either ``'point'`` or ``'cell'``.

        title : str, optional
            The string label of the slider widget.

        pointa : sequence, default: (0.4, 0.9)
            The relative coordinates of the left point of the slider
            on the display port.

        pointb : sequence, default: (0.9, 0.9)
            The relative coordinates of the right point of the slider
            on the display port.

        continuous : bool, default: False
            If this is enabled (default is ``False``), use the continuous
            interval ``[minimum cell scalar, maximum cell scalar]``
            to intersect the threshold bound, rather than the set of
            discrete scalar values from the vertices.

        all_scalars : bool, default: False
            If using scalars from point data, all
            points in a cell must satisfy the threshold when this
            value is ``True``.  When ``False``, any point of the cell
            with a scalar value satisfying the threshold criterion
            will extract the cell. Has no effect when using cell data.

        method : str, default: 'upper'
            Set the threshold method for single-values, defining which
            threshold bounds to use. If the ``value`` is a range, this
            parameter will be ignored, extracting data between the two
            values. For single values, ``'lower'`` will extract data
            lower than the  ``value``. ``'upper'`` will extract data
            larger than the ``value``.

        **kwargs : dict, optional
            All additional keyword arguments are passed to ``add_mesh`` to
            control how the mesh is displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        # avoid circular import
        from ..core.filters.data_set import _set_threshold_limit

        mesh, algo = algorithm_to_mesh_handler(mesh)

        if isinstance(mesh, pyvista.PointSet):
            # vtkThreshold is CELL-wise and PointSets have no cells
            algo = pointset_to_polydata_algorithm(algo or mesh)
            mesh, algo = algorithm_to_mesh_handler(algo)

        if isinstance(mesh, pyvista.MultiBlock):
            raise TypeError('MultiBlock datasets are not supported for threshold widget.')
        name = kwargs.get('name', mesh.memory_address)
        if scalars is None:
            field, scalars = mesh.active_scalars_info
        arr = get_array(mesh, scalars, preference=preference)
        if arr is None:
            raise ValueError('No arrays present to threshold.')
        field = get_array_association(mesh, scalars, preference=preference)

        rng = mesh.get_data_range(scalars)
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        if title is None:
            title = scalars
        mesh.set_active_scalars(scalars)

        self.add_mesh(outline_algorithm(algo or mesh), name=f"{name}-outline", opacity=0.0)

        alg = _vtk.vtkThreshold()
        set_algorithm_input(alg, algo or mesh)
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        alg.SetUseContinuousCellRange(continuous)
        alg.SetAllScalars(all_scalars)

        threshold_mesh = pyvista.wrap(alg.GetOutput())
        self.threshold_meshes.append(threshold_mesh)

        def callback(value):
            _set_threshold_limit(alg, value, method, invert)
            alg.Update()
            threshold_mesh.shallow_copy(alg.GetOutput())

        self.add_slider_widget(
            callback=callback,
            rng=rng,
            title=title,
            color=widget_color,
            pointa=pointa,
            pointb=pointb,
        )

        kwargs.setdefault("reset_camera", False)
        return self.add_mesh(alg, scalars=scalars, **kwargs)

    def add_mesh_isovalue(
        self,
        mesh,
        scalars=None,
        compute_normals=False,
        compute_gradients=False,
        compute_scalars=True,
        preference='point',
        title=None,
        pointa=(0.4, 0.9),
        pointb=(0.9, 0.9),
        widget_color=None,
        **kwargs,
    ):
        """Create a contour of a mesh with a slider.

        Add a mesh to the scene with a slider widget that is used to
        contour at an isovalue of the *point* data on the mesh
        interactively.

        The isovalue mesh is saved to the ``.isovalue_meshes``
        attribute on the plotter.

        .. warning::
            This will not work with :class:`pyvista.PointSet` as
            creating an isovalue is a dimension reducing operation
            on the geometry and point clouds are zero dimensional.
            This will similarly fail for point clouds in
            :class:`pyvista.PolyData`.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and contour or algorithm
            that produces said mesh.

        scalars : str, optional
            The string name of the scalars on the mesh to contour and display.

        compute_normals : bool, optional
            Enable or disable the computation of normals.  If the
            output data will be processed by filters that modify
            topology or geometry, it may be wise to disable computing
            normals.

        compute_gradients : bool, optional
            Enable or disable the computation of gradients.  If the
            output data will be processed by filters that modify
            topology or geometry, it may be wise to disable computing
            gradients.

        compute_scalars : bool, optional
            Enable or disable the computation of scalars.

        preference : str, optional
            When ``mesh.n_points == mesh.n_cells`` and setting
            scalars, this parameter sets how the scalars will be
            mapped to the mesh.  Default ``'point'``, causes the
            scalars will be associated with the mesh points.  Can be
            either ``'point'`` or ``'cell'``.

        title : str, optional
            The string label of the slider widget.

        pointa : sequence, optional
            The relative coordinates of the left point of the slider
            on the display port.

        pointb : sequence
            The relative coordinates of the right point of the slider
            on the display port.

        widget_color : ColorLike, optional
            Color of the widget.  Either a string, RGB sequence, or
            hex color string.  For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        mesh, algo = algorithm_to_mesh_handler(mesh)
        if isinstance(mesh, pyvista.PointSet):
            raise TypeError('PointSets are 0-dimensional and thus cannot produce contours.')
        if isinstance(mesh, pyvista.MultiBlock):
            raise TypeError('MultiBlock datasets are not supported for this widget.')
        name = kwargs.get('name', mesh.memory_address)
        # set the array to contour on
        if mesh.n_arrays < 1:
            raise ValueError('Input dataset for the contour filter must have data arrays.')
        if scalars is None:
            field, scalars = mesh.active_scalars_info
        else:
            field = get_array_association(mesh, scalars, preference=preference)
        # NOTE: only point data is allowed? well cells works but seems buggy?
        if field != pyvista.FieldAssociation.POINT:
            raise TypeError(
                f'Contour filter only works on Point data. Array ({scalars}) is in the Cell data.'
            )

        rng = mesh.get_data_range(scalars)
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        if title is None:
            title = scalars
        mesh.set_active_scalars(scalars)

        alg = _vtk.vtkContourFilter()
        set_algorithm_input(alg, algo or mesh)
        alg.SetComputeNormals(compute_normals)
        alg.SetComputeGradients(compute_gradients)
        alg.SetComputeScalars(compute_scalars)
        alg.SetInputArrayToProcess(0, 0, 0, field.value, scalars)
        alg.SetNumberOfContours(1)  # Only one contour level

        self.add_mesh(outline_algorithm(algo or mesh), name=f"{name}-outline", opacity=0.0)

        isovalue_mesh = pyvista.wrap(alg.GetOutput())
        self.isovalue_meshes.append(isovalue_mesh)

        def callback(value):
            alg.SetValue(0, value)
            alg.Update()
            isovalue_mesh.shallow_copy(alg.GetOutput())

        self.add_slider_widget(
            callback=callback,
            rng=rng,
            title=title,
            color=widget_color,
            pointa=pointa,
            pointb=pointb,
        )

        kwargs.setdefault("reset_camera", False)
        return self.add_mesh(alg, scalars=scalars, **kwargs)

    def add_spline_widget(
        self,
        callback,
        bounds=None,
        factor=1.25,
        n_handles=5,
        resolution=25,
        color="yellow",
        show_ribbon=False,
        ribbon_color="pink",
        ribbon_opacity=0.5,
        pass_widget=False,
        closed=False,
        initial_points=None,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
    ):
        """Create and add a spline widget to the scene.

        Use the bounds argument to place this widget. Several "handles" are
        used to control a parametric function for building this spline. Click
        directly on the line to translate the widget.

        Parameters
        ----------
        callback : callable
            The method called every time the spline is updated. This passes a
            :class:`pyvista.PolyData` object to the callback function of the
            generated spline.

        bounds : sequence[float], optional
            Length 6 sequence of the bounding box where the widget is placed.

        factor : float, optional
            An inflation factor to expand on the bounds when placing.

        n_handles : int, optional
            The number of interactive spheres to control the spline's
            parametric function.

        resolution : int, optional
            The number of points in the spline created between all the handles.

        color : ColorLike, optional
            Either a string, RGB sequence, or hex color string.

        show_ribbon : bool, optional
            If ``True``, the poly plane used for slicing will also be shown.

        ribbon_color : ColorLike, optional
            Color of the ribbon.  Either a string, RGB sequence, or
            hex color string.

        ribbon_opacity : float, optional
            Opacity of ribbon. Defaults to 1.0 and must be between
            ``[0, 1]``.

        pass_widget : bool, optional
            If ``True``, the widget will be passed as the last argument of the
            callback.

        closed : bool, optional
            Make the spline a closed loop.

        initial_points : sequence, optional
            The points to initialize the widget placement. Must have
            same number of elements as ``n_handles``. If the first and
            last point are the same, this will be a closed loop
            spline.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        Returns
        -------
        vtk.vtkSplineWidget
            The newly created spline widget.

        Notes
        -----
        This widget has trouble displaying certain colors. Use only simple
        colors (white, black, yellow).

        """
        if initial_points is not None and len(initial_points) != n_handles:
            raise ValueError("`initial_points` must be length `n_handles`.")

        color = Color(color, default_color=pyvista.global_theme.color)

        if bounds is None:
            bounds = self.bounds

        ribbon = pyvista.PolyData()

        def _the_callback(widget, event_id):
            para_source = _vtk.vtkParametricFunctionSource()
            para_source.SetParametricFunction(widget.GetParametricSpline())
            para_source.Update()
            polyline = pyvista.wrap(para_source.GetOutput())
            ribbon.shallow_copy(polyline.ribbon(normal=(0, 0, 1), angle=90.0))
            if callable(callback):
                if pass_widget:
                    try_callback(callback, polyline, widget)
                else:
                    try_callback(callback, polyline)
            return

        spline_widget = _vtk.vtkSplineWidget()
        spline_widget.GetLineProperty().SetColor(color.float_rgb)
        spline_widget.SetNumberOfHandles(n_handles)
        spline_widget.SetInteractor(self.iren.interactor)
        spline_widget.SetCurrentRenderer(self.renderer)
        spline_widget.SetPlaceFactor(factor)
        spline_widget.PlaceWidget(bounds)
        spline_widget.SetResolution(resolution)
        if initial_points is not None:
            spline_widget.InitializeHandles(pyvista.vtk_points(initial_points))
        else:
            spline_widget.SetClosed(closed)
        spline_widget.Modified()
        spline_widget.On()
        spline_widget.AddObserver(interaction_event, _the_callback)
        _the_callback(spline_widget, None)

        if show_ribbon:
            self.add_mesh(ribbon, color=ribbon_color, opacity=ribbon_opacity)

        self.spline_widgets.append(spline_widget)
        return spline_widget

    def clear_spline_widgets(self):
        """Remove all of the spline widgets."""
        self.spline_widgets.clear()

    def add_mesh_slice_spline(
        self,
        mesh,
        generate_triangles=False,
        n_handles=5,
        resolution=25,
        widget_color=None,
        show_ribbon=False,
        ribbon_color="pink",
        ribbon_opacity=0.5,
        initial_points=None,
        closed=False,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
        **kwargs,
    ):
        """Slice a mesh with a spline widget.

        Add a mesh to the scene with a spline widget that is used to slice
        the mesh interactively.

        The sliced mesh is saved to the ``.spline_sliced_meshes`` attribute on
        the plotter.

        Parameters
        ----------
        mesh : pyvista.DataSet or vtk.vtkAlgorithm
            The input dataset to add to the scene and slice along the spline
            or algorithm that produces said mesh.

        generate_triangles : bool, optional
            If this is enabled (``False`` by default), the output will be
            triangles otherwise, the output will be the intersection polygons.

        n_handles : int, optional
            The number of interactive spheres to control the spline's
            parametric function.

        resolution : int, optional
            The number of points to generate on the spline.

        widget_color : ColorLike, optional
            Color of the widget.  Either a string, RGB sequence, or
            hex color string.  For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        show_ribbon : bool, optional
            If ``True``, the poly plane used for slicing will also be shown.

        ribbon_color : ColorLike, optional
            Color of the ribbon.  Either a string, RGB sequence, or
            hex color string.

        ribbon_opacity : float, optional
            Opacity of ribbon. Defaults to 1.0 and must be between
            ``[0, 1]``.

        initial_points : sequence, optional
            The points to initialize the widget placement. Must have same
            number of elements as ``n_handles``. If the first and last point
            are the same, this will be a closed loop spline.

        closed : bool, optional
            Make the spline a closed loop.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        **kwargs : dict, optional
            All additional keyword arguments are passed to
            :func:`Plotter.add_mesh` to control how the mesh is
            displayed.

        Returns
        -------
        vtk.vtkActor
            VTK actor of the mesh.

        """
        mesh, algo = algorithm_to_mesh_handler(mesh)
        name = kwargs.get('name', None)
        if name is None:
            name = mesh.memory_address
        rng = mesh.get_data_range(kwargs.get('scalars', None))
        kwargs.setdefault('clim', kwargs.pop('rng', rng))
        mesh.set_active_scalars(kwargs.get('scalars', mesh.active_scalars_name))

        self.add_mesh(outline_algorithm(algo or mesh), name=f"{name}-outline", opacity=0.0)

        alg = _vtk.vtkCutter()  # Construct the cutter object
        # Use the grid as the data we desire to cut
        set_algorithm_input(alg, algo or mesh)
        if not generate_triangles:
            alg.GenerateTrianglesOff()

        spline_sliced_mesh = pyvista.wrap(alg.GetOutput())
        self.spline_sliced_meshes.append(spline_sliced_mesh)

        def callback(spline):
            polyline = spline.GetCell(0)
            # create the plane for clipping
            polyplane = _vtk.vtkPolyPlane()
            polyplane.SetPolyLine(polyline)
            alg.SetCutFunction(polyplane)  # the cutter to use the poly planes
            alg.Update()  # Perform the Cut
            spline_sliced_mesh.shallow_copy(alg.GetOutput())

        self.add_spline_widget(
            callback=callback,
            bounds=mesh.bounds,
            factor=1.25,
            color=widget_color,
            n_handles=n_handles,
            resolution=resolution,
            show_ribbon=show_ribbon,
            ribbon_color=ribbon_color,
            ribbon_opacity=ribbon_opacity,
            initial_points=initial_points,
            closed=closed,
            interaction_event=interaction_event,
        )

        return self.add_mesh(alg, **kwargs)

    def add_sphere_widget(
        self,
        callback,
        center=(0, 0, 0),
        radius=0.5,
        theta_resolution=30,
        phi_resolution=30,
        color=None,
        style="surface",
        selected_color="pink",
        indices=None,
        pass_widget=False,
        test_callback=True,
        interaction_event=_vtk.vtkCommand.EndInteractionEvent,
    ):
        """Add one or many sphere widgets to a scene.

        Use a sphere widget to control a vertex location.

        Parameters
        ----------
        callback : callable
            The function to call back when the widget is modified. It takes a
            single argument: the center of the sphere as an XYZ coordinate (a
            3-length sequence), unless ``pass_widget=True``, in which case the
            callback must accept the widget object as the second parameter.  If
            multiple centers are passed in the ``center`` parameter, the
            callback must also accept an index of that widget.

        center : sequence[float], optional
            The cartesian coordinate of the sphere's center when placing it in
            the scene. If more than one location is passed, then that many
            widgets will be added and the callback will also be passed the
            integer index of that widget.

        radius : float, optional
            The radius of the sphere.

        theta_resolution : int, optional
            Set the number of points in the longitude direction.

        phi_resolution : int, optional
            Set the number of points in the latitude direction.

        color : ColorLike, optional
            The color of the sphere's surface.  If multiple centers
            are passed, then this must be a list of colors.  Each
            color is either a string, rgb list, or hex color string.
            For example:

            * ``color='white'``
            * ``color='w'``
            * ``color=[1.0, 1.0, 1.0]``
            * ``color='#FFFFFF'``

        style : str, optional
            Representation style: ``'surface'`` or ``'wireframe'``.

        selected_color : ColorLike, optional
            Color of the widget when selected during interaction.

        indices : sequence[int], optional
            Indices to assign the sphere widgets.

        pass_widget : bool, optional
            If ``True``, the widget will be passed as the last
            argument of the callback.

        test_callback : bool, optional
            If ``True``, run the callback function after the widget is
            created.

        interaction_event : vtk.vtkCommand.EventIds, optional
            The VTK interaction event to use for triggering the callback.

        Returns
        -------
        vtk.vtkSphereWidget
            The sphere widget.

        """
        if color is None:
            color = pyvista.global_theme.color.float_rgb
        selected_color = Color(selected_color)

        center = np.array(center)
        num = 1
        if center.ndim > 1:
            num = len(center)

        if isinstance(color, (list, tuple, np.ndarray)):
            if len(color) == num and not isinstance(color[0], float):
                colors = color
            else:
                colors = [color] * num
        else:
            colors = [color] * num

        def _the_callback(widget, event_id):
            point = widget.GetCenter()
            index = widget.WIDGET_INDEX
            if callable(callback):
                if num > 1:
                    args = [point, index]
                else:
                    args = [point]
                if pass_widget:
                    args.append(widget)
                try_callback(callback, *args)
            return

        if indices is None:
            indices = [x for x in range(num)]

        for i in range(num):
            if center.ndim > 1:
                loc = center[i]
            else:
                loc = center
            sphere_widget = _vtk.vtkSphereWidget()
            sphere_widget.WIDGET_INDEX = indices[i]  # Monkey patch the index
            if style in "wireframe":
                sphere_widget.SetRepresentationToWireframe()
            else:
                sphere_widget.SetRepresentationToSurface()
            sphere_widget.GetSphereProperty().SetColor(Color(colors[i]).float_rgb)
            sphere_widget.GetSelectedSphereProperty().SetColor(selected_color.float_rgb)
            sphere_widget.SetInteractor(self.iren.interactor)
            sphere_widget.SetCurrentRenderer(self.renderer)
            sphere_widget.SetRadius(radius)
            sphere_widget.SetCenter(loc)
            sphere_widget.SetThetaResolution(theta_resolution)
            sphere_widget.SetPhiResolution(phi_resolution)
            sphere_widget.Modified()
            sphere_widget.On()
            sphere_widget.AddObserver(interaction_event, _the_callback)
            self.sphere_widgets.append(sphere_widget)

        if test_callback is True:
            # Test call back in the last
            _the_callback(sphere_widget, None)
        if num > 1:
            return self.sphere_widgets

        return sphere_widget

    def clear_sphere_widgets(self):
        """Remove all of the sphere widgets."""
        self.sphere_widgets.clear()

    def add_checkbox_button_widget(
        self,
        callback,
        value=False,
        position=(10.0, 10.0),
        size=50,
        border_size=5,
        color_on='blue',
        color_off='grey',
        background_color='white',
    ):
        """Add a checkbox button widget to the scene.

        This is useless without a callback function. You can pass a callable
        function that takes a single argument, the state of this button widget
        and performs a task with that value.

        Parameters
        ----------
        callback : callable
            The method called every time the button is clicked. This should take
            a single parameter: the bool value of the button.

        value : bool, default: False
            The default state of the button.

        position : sequence[float], default: (10.0, 10.0)
            The absolute coordinates of the bottom left point of the button.

        size : int, default: 50
            The size of the button in number of pixels.

        border_size : int, default: 5
            The size of the borders of the button in pixels.

        color_on : ColorLike, optional
            The color used when the button is checked. Default is ``'blue'``.

        color_off : ColorLike, optional
            The color used when the button is not checked. Default is ``'grey'``.

        background_color : ColorLike, optional
            The background color of the button. Default is ``'white'``.

        Returns
        -------
        vtk.vtkButtonWidget
            The VTK button widget configured as a checkbox button.

        Examples
        --------
        The following example generates a static image of the widget.

        >>> import pyvista as pv
        >>> mesh = pv.Sphere()
        >>> p = pv.Plotter()
        >>> actor = p.add_mesh(mesh)
        >>> def toggle_vis(flag):
        ...     actor.SetVisibility(flag)
        ...
        >>> _ = p.add_checkbox_button_widget(toggle_vis, value=True)
        >>> p.show()

        Download the interactive example at :ref:`checkbox_widget_example`.

        """
        if self.iren is None:  # pragma: no cover
            raise RuntimeError('Cannot add a widget to a closed plotter.')

        def create_button(color1, color2, color3, dims=(size, size, 1)):
            color1 = np.array(Color(color1).int_rgb)
            color2 = np.array(Color(color2).int_rgb)
            color3 = np.array(Color(color3).int_rgb)

            n_points = dims[0] * dims[1]
            button = pyvista.UniformGrid(dimensions=dims)
            arr = np.array([color1] * n_points).reshape(dims[0], dims[1], 3)  # fill with color1
            arr[1 : dims[0] - 1, 1 : dims[1] - 1] = color2  # apply color2
            arr[
                border_size : dims[0] - border_size, border_size : dims[1] - border_size
            ] = color3  # apply color3
            button.point_data['texture'] = arr.reshape(n_points, 3).astype(np.uint8)
            return button

        button_on = create_button(color_on, background_color, color_on)
        button_off = create_button(color_on, background_color, color_off)

        bounds = [position[0], position[0] + size, position[1], position[1] + size, 0.0, 0.0]

        button_rep = _vtk.vtkTexturedButtonRepresentation2D()
        button_rep.SetNumberOfStates(2)
        button_rep.SetState(value)
        button_rep.SetButtonTexture(0, button_off)
        button_rep.SetButtonTexture(1, button_on)
        button_rep.SetPlaceFactor(1)
        button_rep.PlaceWidget(bounds)

        button_widget = _vtk.vtkButtonWidget()
        button_widget.SetInteractor(self.iren.interactor)
        button_widget.SetRepresentation(button_rep)
        button_widget.SetCurrentRenderer(self.renderer)
        button_widget.On()

        def _the_callback(widget, event):
            state = widget.GetRepresentation().GetState()
            if callable(callback):
                try_callback(callback, bool(state))

        button_widget.AddObserver(_vtk.vtkCommand.StateChangedEvent, _the_callback)
        self.button_widgets.append(button_widget)
        return button_widget

    def add_camera_orientation_widget(self, animate=True, n_frames=20):
        """Add a camera orientation widget to the active renderer.

        .. note::
           This widget requires ``vtk>=9.1.0``.

        Parameters
        ----------
        animate : bool, default: True
            Enable or disable jump-to-axis-view animation.

        n_frames : int, default: 20
            The number of frames to animate the jump-to-axis-viewpoint feature.

        Returns
        -------
        vtkCameraOrientationWidget
            Camera orientation widget.

        Examples
        --------
        Add a camera orientation widget to the scene.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> plotter = pyvista.Plotter()
        >>> _ = plotter.add_mesh(
        ...     mesh, scalars=range(6), show_scalar_bar=False
        ... )
        >>> _ = plotter.add_camera_orientation_widget()
        >>> plotter.show()

        """
        try:
            from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationWidget
        except ImportError:  # pragma: no cover
            from pyvista.core.errors import VTKVersionError

            raise VTKVersionError('vtkCameraOrientationWidget requires vtk>=9.1.0')

        widget = vtkCameraOrientationWidget()
        widget.SetParentRenderer(self.renderer)
        widget.SetAnimate(animate)
        widget.SetAnimatorTotalFrames(n_frames)
        widget.On()
        self.camera_widgets.append(widget)
        return widget

    def clear_camera_widgets(self):
        """Remove all of the camera widgets."""
        self.camera_widgets.clear()

    def clear_button_widgets(self):
        """Remove all of the button widgets."""
        self.button_widgets.clear()

    def close(self):
        """Close the widgets."""
        self.clear_box_widgets()
        self.clear_plane_widgets()
        self.clear_line_widgets()
        self.clear_slider_widgets()
        self.clear_sphere_widgets()
        self.clear_spline_widgets()
        self.clear_button_widgets()
        self.clear_camera_widgets()


# pyvista/trame/__init__.py

"""Trame interface for PyVista."""
from pyvista.trame.jupyter import launch_server, show_trame, elegantly_launch
from pyvista.trame.ui import get_or_create_viewer, plotter_ui
from pyvista.trame.views import PyVistaLocalView, PyVistaRemoteLocalView, PyVistaRemoteView


# __all__ only left for mypy --strict to work when pyvista is a dependency
__all__ = [
    'elegantly_launch',
    'get_or_create_viewer',
    'launch_server',
    'plotter_ui',
    'show_trame',
    'PyVistaLocalView',
    'PyVistaRemoteLocalView',
    'PyVistaRemoteView',
]


# pyvista/trame/jupyter.py

"""Trame utilities for running in Jupyter."""
import asyncio
import logging
import os
import warnings

from trame.app import get_server
from trame.ui.vuetify import VAppLayout
from trame.widgets import html as html_widgets, vtk as vtk_widgets, vuetify as vuetify_widgets

try:
    from ipywidgets.widgets import HTML
except ImportError:
    HTML = object


import pyvista
from pyvista.trame.ui import UI_TITLE, get_or_create_viewer
from pyvista.trame.views import CLOSED_PLOTTER_ERROR

SERVER_DOWN_MESSAGE = """Trame server has not launched.

You must start the trame server before attempting to `show()`
with PyVista.

You can use the following snippet to launch the server:

    from pyvista.trame.jupyter import launch_server
    await launch_server('{name}').ready

"""
JUPYTER_SERVER_DOWN_MESSAGE = """Trame server has not launched.

Prior to plotting, please make sure to run `set_jupyter_backend('trame')` when using the `'trame'`, `'server'`, or `'client'` Jupyter backends.

    import pyvista as pv
    pv.set_jupyter_backend('trame')

If this issue persists, please open an issue in PyVista: https://github.com/pyvista/pyvista/issues

"""

logger = logging.getLogger(__name__)


class TrameServerDownError(RuntimeError):
    """Exception when trame server is down for Jupyter."""

    def __init__(self, server_name):
        """Call the base class constructor with the custom message."""
        super().__init__(SERVER_DOWN_MESSAGE.format(name=server_name))


class TrameJupyterServerDownError(RuntimeError):
    """Exception when trame server is down for Jupyter."""

    def __init__(self):
        """Call the base class constructor with the custom message."""
        # Be incredibly verbose on how users should launch trame server
        # Both warn so it appears at top
        warnings.warn(JUPYTER_SERVER_DOWN_MESSAGE)
        # and Error
        super().__init__(JUPYTER_SERVER_DOWN_MESSAGE)


class Widget(HTML):
    """Custom HTML iframe widget for trame viewer."""

    def __init__(self, viewer, src, width, height, **kwargs):
        """Initialize."""
        if HTML is object:
            raise ImportError('Please install `ipywidgets`.')
        value = f"<iframe src='{src}' style='width: {width}; height: {height};'></iframe>"
        super().__init__(value, **kwargs)
        self._viewer = viewer
        self._src = src

    @property
    def viewer(self):
        """Get the associated viewer instance."""
        return self._viewer

    @property
    def src(self):
        """Get the src URL."""
        return self._src


def launch_server(server=None, port=None, host=None):
    """Launch a trame server for use with Jupyter.

    Parameters
    ----------
    server : str, optional
        By default this uses :attr:`pyvista.global_theme.trame.jupyter_server_name
        <pyvista.themes._TrameConfig.jupyter_server_name>`, which by default is
        set to ``'pyvista-jupyter'``.

        If a server name is given and such server is not available yet, it will
        be created otherwise the previously created instance will be returned.

    port : int, optional
        The port on which to bind the server. Defaults to 0 to automatically
        find an available port.

    host : str, optional
        The host name to bind the server to on launch. Server will bind to
        ``127.0.0.1`` by default unless user sets the environment variable ``TRAME_DEFAULT_HOST``.

    Returns
    -------
    trame_server.core.Server
        The launched Trame server. To ``await`` the launch, use the
        ``.ready`` future attribute on the server.

    """
    if server is None:
        server = pyvista.global_theme.trame.jupyter_server_name
    if isinstance(server, str):
        server = get_server(server)
    if port is None:
        port = pyvista.global_theme.trame.jupyter_server_port
    if host is None:
        # Default to `127.0.0.1` unless user sets TRAME_DEFAULT_HOST
        host = os.environ.get('TRAME_DEFAULT_HOST', '127.0.0.1')

    # Must enable all used modules
    html_widgets.initialize(server)
    vtk_widgets.initialize(server)
    vuetify_widgets.initialize(server)

    def on_ready(**_):
        logger.debug(f'Server ready: {server}')

    if server._running_stage == 0:
        server.controller.on_server_ready.add(on_ready)
        server.start(
            exec_mode='task',
            host=host,
            port=port,
            open_browser=False,
            show_connection_info=False,
            disable_logging=True,
            timeout=0,
        )
    # else, server is already running or launching
    return server


def build_url(
    _server,
    ui=None,
    server_proxy_enabled=None,
    server_proxy_prefix=None,
    host='localhost',
    protocol='http',
):
    """Build the URL for the iframe."""
    params = f'?ui={ui}&reconnect=auto' if ui else '?reconnect=auto'
    if server_proxy_enabled is None:
        server_proxy_enabled = pyvista.global_theme.trame.server_proxy_enabled
    if server_proxy_enabled:
        if server_proxy_prefix is None:
            server_proxy_prefix = pyvista.global_theme.trame.server_proxy_prefix
        # server_proxy_prefix assumes trailing slash
        src = (
            f"{server_proxy_prefix if server_proxy_prefix else ''}{_server.port}/index.html{params}"
        )
    else:
        src = f'{protocol}://{host}:{_server.port}/index.html{params}'
    logger.debug(src)
    return src


def initialize(
    server, plotter, mode=None, default_server_rendering=True, collapse_menu=False, **kwargs
):
    """Generate the UI for a given plotter."""
    state = server.state
    state.trame__title = UI_TITLE

    viewer = get_or_create_viewer(plotter, suppress_rendering=mode == 'client')

    with VAppLayout(server, template_name=plotter._id_name):
        viewer.ui(
            mode=mode,
            default_server_rendering=default_server_rendering,
            collapse_menu=collapse_menu,
            **kwargs,
        )

    return viewer


def show_trame(
    plotter,
    mode=None,
    name=None,
    server_proxy_enabled=None,
    server_proxy_prefix=None,
    collapse_menu=False,
    add_menu=True,
    default_server_rendering=True,
    handler=None,
    **kwargs,
):
    """Run and display the trame application in jupyter's event loop.

    Parameters
    ----------
    plotter : pyvista.Plotter
        The PyVista plotter to show.

    mode : str, optional
        The UI view mode. This can be set on the global theme. Options are:

        * ``'trame'``: Uses a view that can switch between client and server
          rendering modes.
        * ``'server'``: Uses a view that is purely server rendering.
        * ``'client'``: Uses a view that is purely client rendering (generally
          safe without a virtual frame buffer)

    name : str, optional
        The name of the trame server on which the UI is defined.

    server_proxy_enabled : bool, default: False
        Build a relative URL for use with ``jupyter-server-proxy``.

    server_proxy_prefix : str, optional
        URL prefix when using ``server_proxy_enabled``. This can be set
        globally in the theme. To ignore, pass ``False``. For use with
        ``jupyter-server-proxy``, often set to ``proxy/``.

    collapse_menu : bool, default: False
        Collapse the UI menu (camera controls, etc.) on start.

    add_menu : bool, default: True
        Add a UI controls VCard to the VContainer.

    default_server_rendering : bool, default: True
        Whether to use server-side or client-side rendering on-start when
        using the ``'trame'`` mode.

    handler : callable, optional
        Pass a callable that accptes the viewer instance, the string URL,
        and ``**kwargs`` to create custom HTML representations of the output.

        .. code:: python

            import pyvista as pv
            from IPython.display import IFrame

            mesh = pv.Wavelet()


            def handler(viewer, src, **kwargs):
                return IFrame(src, '75%', '500px')


            p = pv.Plotter(notebook=True)
            _ = p.add_mesh(mesh)
            iframe = p.show(
                jupyter_backend='trame',
                jupyter_kwargs=dict(handler=handler),
                return_viewer=True,
            )
            iframe

    **kwargs : dict, optional
        Mostly ignored, though ``protocol`` and ``host`` can be use to
        override the iframe src url and ``height`` and ``width`` can be
        used to override the iframe style. Remaining kwargs are passed to
        ``ipywidgets.widgets.HTML``.

    Returns
    -------
    ipywidgets.widgets.HTML or handler result
        Returns a HTML IFrame widget or the result of the passed handler.

    """
    if plotter.render_window is None:
        raise RuntimeError(CLOSED_PLOTTER_ERROR)

    if name is None:
        server = get_server(name=pyvista.global_theme.trame.jupyter_server_name)
    else:
        server = get_server(name=name)
    if name is None and not server.running:
        elegantly_launch(server)
        if not server.running:
            raise TrameJupyterServerDownError()
    elif not server.running:
        raise TrameServerDownError(name)

    # Initialize app
    viewer = initialize(
        server,
        plotter,
        mode=mode,
        default_server_rendering=default_server_rendering,
        collapse_menu=collapse_menu,
        add_menu=add_menu,
    )

    # Show as cell result
    src = build_url(
        server,
        ui=plotter._id_name,
        server_proxy_enabled=server_proxy_enabled,
        server_proxy_prefix=server_proxy_prefix,
        host=kwargs.get('host', 'localhost'),
        protocol=kwargs.get('protocol', 'http'),
    )

    if plotter._window_size_unset:
        dw, dh = '99%', '600px'
    else:
        dw, dh = plotter.window_size
        dw = f'{dw}px'
        dh = f'{dh}px'
    kwargs.setdefault('width', dw)
    kwargs.setdefault('height', dh)

    if callable(handler):
        return handler(viewer, src, **kwargs)
    return Widget(viewer, src, **kwargs)


def elegantly_launch(*args, **kwargs):
    """Elegantly launch the Trame server without await.

    This provides a mechanism to launch the Trame Jupyter backend in
    a way that does not require users to await the call.

    This is a thin wrapper of
    :func:`launch_server() <pyvista.trame.jupyter.launch_server>`.

    Returns
    -------
    trame_server.core.Server
        The launched trame server.

    Warnings
    --------
    This uses `nest_asyncio <https://github.com/erdewit/nest_asyncio>`_ which
    patches the standard lib `asyncio` package and may have unintended
    consequences for some uses cases. We advise strongly to make sure PyVista's
    Jupyter backend is not set to use Trame when not in a Jupyter environment.

    """
    try:
        import nest_asyncio
    except ImportError:
        raise ImportError(
            """Please install `nest_asyncio` to automagically launch the trame server without await. Or, to avoid `nest_asynctio` run:

    from pyvista.trame.jupyter import launch_server
    await launch_server().ready
"""
        )

    async def launch_it():
        await launch_server(*args, **kwargs).ready

    # Basically monkey patches asyncio to support this
    nest_asyncio.apply()

    return asyncio.run(launch_it())


# pyvista/trame/ui.py

# flake8: noqa: D102,D103,D107
"""PyVista Trame User Interface.

This module builds a base UI for manipulating a PyVista Plotter.
The UI generated here is the default for rendering in Jupyter
environments and provides a starting point for custom user-built
applications.
"""
import io

from trame.widgets import html, vuetify

import pyvista
from pyvista.trame.views import PyVistaLocalView, PyVistaRemoteLocalView, PyVistaRemoteView

UI_TITLE = 'PyVista'

VALID_UI_MODES = [
    'trame',
    'client',
    'server',
]

_VIEWERS = {}


def button(click, icon, tooltip):
    """Create a vuetify button."""
    with vuetify.VTooltip(bottom=True):
        with vuetify.Template(v_slot_activator='{ on, attrs }'):
            with vuetify.VBtn(icon=True, v_bind='attrs', v_on='on', click=click):
                vuetify.VIcon(icon)
        html.Span(tooltip)


def checkbox(model, icons, tooltip):
    """Create a vuetify checkbox."""
    with vuetify.VTooltip(bottom=True):
        with vuetify.Template(v_slot_activator='{ on, attrs }'):
            with html.Div(v_on='on', v_bind='attrs'):
                vuetify.VCheckbox(
                    v_model=model,
                    on_icon=icons[0],
                    off_icon=icons[1],
                    dense=True,
                    hide_details=True,
                    classes='my-0 py-0 ml-1',
                )
        html.Span(tooltip)


class Viewer:
    """Internal wrapper to sync trame view with Plotter."""

    def __init__(self, plotter, suppress_rendering=False):
        """Initialize Viewer."""
        if plotter._id_name in _VIEWERS:
            raise RuntimeError('A viewer instance already exists for this Plotter.')
        _VIEWERS[plotter._id_name] = self
        self._html_views = set()

        self.plotter = plotter
        self.plotter.suppress_rendering = suppress_rendering

        # State variable names
        self.SHOW_UI = f'{plotter._id_name}_show_ui'
        self.GRID = f'{plotter._id_name}_grid_visibility'
        self.OUTLINE = f'{plotter._id_name}_outline_visibility'
        self.EDGES = f'{plotter._id_name}_edge_visibility'
        self.AXIS = f'{plotter._id_name}_axis_visiblity'
        self.SERVER_RENDERING = f'{plotter._id_name}_use_server_rendering'

    @property
    def views(self):
        """Get a set of all associate trame views for this viewer."""
        return self._html_views

    def update(self, **kwargs):
        """Update all associated views.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        for view in self._html_views:
            view.update()

    def push_camera(self, **kwargs):
        """Push camera to all associated views.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        for view in self._html_views:
            view.push_camera()

    def reset_camera(self, **kwargs):
        """Reset camera for all associated views.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        for view in self._html_views:
            view.reset_camera()

    def update_image(self, **kwargs):
        """Update image for all associated views.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        for view in self._html_views:
            view.update_image()

    def update_camera(self, **kwargs):
        """Update image and camera for all associated views.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        for view in self._html_views:
            view.update_camera()

    def view_isometric(self):
        """View isometric."""
        self.plotter.view_isometric(render=False)
        self.update_camera()

    def view_yz(self):
        """View YZ plane."""
        self.plotter.view_yz(render=False)
        self.update_camera()

    def view_xz(self):
        """View XZ plane."""
        self.plotter.view_xz(render=False)
        self.update_camera()

    def view_xy(self):
        """View XY plane."""
        self.plotter.view_xy(render=False)
        self.update_camera()

    def on_edge_visiblity_change(self, **kwargs):
        """Toggle edge visibility for all actors.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        value = kwargs[self.EDGES]
        for renderer in self.plotter.renderers:
            for _, actor in renderer.actors.items():
                if isinstance(actor, pyvista.Actor):
                    actor.prop.show_edges = value
        self.update()

    def on_grid_visiblity_change(self, **kwargs):
        """Handle axes grid visibility.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        value = kwargs[self.GRID]
        for renderer in self.plotter.renderers:
            if value:
                renderer.show_grid()
            else:
                renderer.remove_bounds_axes()
        self.update()

    def on_outline_visiblity_change(self, **kwargs):
        """Handle outline visibility.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        value = kwargs[self.OUTLINE]
        for renderer in self.plotter.renderers:
            if value:
                renderer.add_bounding_box(reset_camera=False)
            else:
                renderer.remove_bounding_box()
        self.update()

    def on_axis_visiblity_change(self, **kwargs):
        """Handle outline visibility.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        value = kwargs[self.AXIS]
        for renderer in self.plotter.renderers:
            if value:
                renderer.show_axes()
            else:
                renderer.hide_axes()
        self.update()

    def on_rendering_mode_change(self, **kwargs):
        """Handle any configurations when the render mode changes between client and server.

        Parameters
        ----------
        **kwargs : dict, optional
            Unused keyword arguments.

        """
        if not kwargs[self.SERVER_RENDERING]:
            self.update_camera()

    @property
    def actors(self):
        """Get dataset actors."""
        return {k: v for k, v in self.plotter.actors.items() if isinstance(v, pyvista.Actor)}

    def screenshot(self):
        """Take screenshot and add attachament.

        Returns
        -------
        memoryview
            Screenshot as a ``memoryview``.

        """
        self.plotter.render()
        self.update()  # makes sure the plotter and views are in sync
        buffer = io.BytesIO()
        self.plotter.screenshot(filename=buffer)
        buffer.seek(0)
        return memoryview(buffer.read())

    def ui_controls(self, mode=None, default_server_rendering=True, v_show=None):
        """Create a VRow for the UI controls.

        Parameters
        ----------
        mode : str, default: 'trame'
            The UI view mode. Options are:

            * ``'trame'``: Uses a view that can switch between client and server
              rendering modes.
            * ``'server'``: Uses a view that is purely server rendering.
            * ``'client'``: Uses a view that is purely client rendering (generally
              safe without a virtual frame buffer)

        default_server_rendering : bool, default: True
            Whether to use server-side or client-side rendering on-start when
            using the ``'trame'`` mode.

        v_show : bool, optional
            Conditionally show the viewer controls.

        """
        if mode is None:
            mode = self.plotter._theme.trame.default_mode
        if mode not in VALID_UI_MODES:
            raise ValueError(f'`{mode}` is not a valid mode choice. Use one of: {VALID_UI_MODES}')
        if mode != 'trame':
            default_server_rendering = mode == 'server'

        with vuetify.VRow(
            v_show=v_show,
            classes='pa-0 ma-0 align-center',
        ) as row:
            server = row.server
            # Listen to state changes
            server.state.change(self.EDGES)(self.on_edge_visiblity_change)
            server.state.change(self.GRID)(self.on_grid_visiblity_change)
            server.state.change(self.OUTLINE)(self.on_outline_visiblity_change)
            server.state.change(self.AXIS)(self.on_axis_visiblity_change)
            server.state.change(self.SERVER_RENDERING)(self.on_rendering_mode_change)
            vuetify.VDivider(vertical=True, classes='mr-1')
            button(
                click=self.reset_camera,
                icon='mdi-arrow-expand-all',
                tooltip='Reset Camera',
            )
            vuetify.VDivider(vertical=True, classes='mx-1')
            button(
                click=self.view_isometric,
                icon='mdi-axis-arrow',
                tooltip='Perspective view',
            )
            button(
                click=self.view_yz,
                icon='mdi-axis-x-arrow',
                tooltip='Reset Camera X',
            )
            button(
                click=self.view_xz,
                icon='mdi-axis-y-arrow',
                tooltip='Reset Camera Y',
            )
            button(
                click=self.view_xy,
                icon='mdi-axis-z-arrow',
                tooltip='Reset Camera Z',
            )
            vuetify.VDivider(vertical=True, classes='mx-1')
            checkbox(
                model=(self.EDGES, False),
                icons=('mdi-grid', 'mdi-grid-off'),
                tooltip=f"Toggle edge visibility ({{{{ {self.EDGES} ? 'on' : 'off' }}}})",
            )
            checkbox(
                model=(self.OUTLINE, False),
                icons=('mdi-cube', 'mdi-cube-off'),
                tooltip=f"Toggle bounding box ({{{{ {self.OUTLINE} ? 'on' : 'off' }}}})",
            )
            checkbox(
                model=(self.GRID, False),
                icons=('mdi-ruler-square', 'mdi-ruler-square'),
                tooltip=f"Toggle ruler ({{{{ {self.GRID} ? 'on' : 'off' }}}})",
            )
            checkbox(
                model=(self.AXIS, False),
                icons=('mdi-axis-arrow-info', 'mdi-axis-arrow-info'),
                tooltip=f"Toggle axis ({{{{ {self.AXIS} ? 'on' : 'off' }}}})",
            )
            # Server rendering options
            if mode == 'trame':
                vuetify.VDivider(vertical=True, classes='mx-1')
                checkbox(
                    model=(self.SERVER_RENDERING, default_server_rendering),
                    icons=('mdi-dns', 'mdi-open-in-app'),
                    tooltip=f"Toggle rendering mode ({{{{ {self.SERVER_RENDERING} ? 'remote' : 'local' }}}})",
                )
            with vuetify.VRow(
                v_show=(self.SERVER_RENDERING, default_server_rendering),
                classes='pa-0 ma-0 align-center',
            ):

                def attach_screenshot():
                    return server.protocol.addAttachment(self.screenshot())

                button(
                    # Must use single-quote string for JS here
                    click=f"utils.download('screenshot.png', trigger('{server.trigger_name(attach_screenshot)}'), 'image/png')",
                    icon='mdi-file-png-box',
                    tooltip='Save screenshot',
                )

    def ui(
        self,
        mode=None,
        default_server_rendering=True,
        collapse_menu=False,
        add_menu=True,
        **kwargs,
    ):
        """Generate VContainer for PyVista Plotter.

        Parameters
        ----------
        mode : str, default: 'trame'
            The UI view mode. Options are:

            * ``'trame'``: Uses a view that can switch between client and server
              rendering modes.
            * ``'server'``: Uses a view that is purely server rendering.
            * ``'client'``: Uses a view that is purely client rendering (generally
              safe without a virtual frame buffer)

        default_server_rendering : bool, default: True
            Whether to use server-side or client-side rendering on-start when
            using the ``'trame'`` mode.

        collapse_menu : bool, default: False
            Collapse the UI menu (camera controls, etc.) on start.

        add_menu : bool, default: True
            Add a UI controls VCard to the VContainer.

        **kwargs : dict, optional
            Additional keyword arguments are passed to the view being created.

        Returns
        -------
        PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
            Trame view interface for pyvista.

        """
        if mode is None:
            mode = self.plotter._theme.trame.default_mode
        if mode not in VALID_UI_MODES:
            raise ValueError(f'`{mode}` is not a valid mode choice. Use one of: {VALID_UI_MODES}')
        if mode != 'trame':
            default_server_rendering = mode == 'server'

        with vuetify.VContainer(
            fluid=True,
            classes='pa-0 fill-height',
        ) as container:
            server = container.server
            # Initialize state variables
            server.state[self.EDGES] = False
            server.state[self.GRID] = self.plotter.renderer.cube_axes_actor is not None
            server.state[self.OUTLINE] = hasattr(self.plotter.renderer, '_box_object')
            server.state[self.AXIS] = (
                hasattr(self.plotter.renderer, 'axes_widget')
                and self.plotter.renderer.axes_widget.GetEnabled()
            )
            server.state[self.SERVER_RENDERING] = default_server_rendering
            if add_menu:
                server.state[self.SHOW_UI] = not collapse_menu
                with vuetify.VCard(
                    style='position: absolute; top: 20px; left: 20px; z-index: 1; height: 36px;',
                    classes=(f"{{ 'rounded-circle': !{self.SHOW_UI} }}",),
                ):
                    with vuetify.VRow(classes='pa-0 ma-0') as row:
                        button(
                            click=f'{self.SHOW_UI}=!{self.SHOW_UI}',
                            icon='mdi-dots-vertical',
                            tooltip=f"{{{{ {self.SHOW_UI} ? 'Hide' : 'Show' }}}} menu",
                        )
                        self.ui_controls(
                            mode=mode,
                            default_server_rendering=default_server_rendering,
                            v_show=(f'{self.SHOW_UI}',),
                        )
            if mode == 'trame':
                view = PyVistaRemoteLocalView(
                    self.plotter,
                    mode=(
                        # Must use single-quote string for JS here
                        f"{self.SERVER_RENDERING} ? 'remote' : 'local'",
                        'remote' if default_server_rendering else 'local',
                    ),
                    **kwargs,
                )
            elif mode == 'server':
                view = PyVistaRemoteView(self.plotter, **kwargs)
            elif mode == 'client':
                view = PyVistaLocalView(self.plotter, **kwargs)

            self._html_views.add(view)

        return view


def get_or_create_viewer(plotter, suppress_rendering=False):
    """Get or create a Viewer instance for a given Plotter.

    There should be only one Viewer instance per plotter. A Viewer
    can have multiple UI views though.

    Parameters
    ----------
    plotter : pyvista.Plotter
        Plotter to return or create the viewer instance for.

    suppress_rendering : bool, default: False
        Suppress rendering on the plotter.

    Returns
    -------
    pyvista.trame.ui.Viewer
        Trame viewer.

    """
    if plotter._id_name in _VIEWERS:
        viewer = _VIEWERS[plotter._id_name]
        if suppress_rendering != plotter.suppress_rendering:
            plotter.suppress_rendering = suppress_rendering
            # TODO: warn user?
        return viewer
    return Viewer(plotter, suppress_rendering=suppress_rendering)


def plotter_ui(
    plotter, mode=None, default_server_rendering=True, collapse_menu=False, add_menu=True, **kwargs
):
    """Create a UI view for the given Plotter.

    Parameters
    ----------
    plotter : pyvista.Plotter
        Plotter to create the UI for.

    mode : str, default: 'trame'
        The UI view mode. Options are:

        * ``'trame'``: Uses a view that can switch between client and server
          rendering modes.
        * ``'server'``: Uses a view that is purely server rendering.
        * ``'client'``: Uses a view that is purely client rendering (generally
          safe without a virtual frame buffer)

    default_server_rendering : bool, default: True
        Whether to use server-side or client-side rendering on-start when
        using the ``'trame'`` mode.

    collapse_menu : bool, default: False
        Collapse the UI menu (camera controls, etc.) on start.

    add_menu : bool, default: True
        Add a UI controls VCard to the VContainer.

    **kwargs : dict, optional
        Additional keyword arguments are passed to the viewer being created.

    Returns
    -------
    PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
        Trame view interface for pyvista.

    """
    viewer = get_or_create_viewer(plotter, suppress_rendering=mode == 'client')
    return viewer.ui(
        mode=mode,
        default_server_rendering=default_server_rendering,
        collapse_menu=collapse_menu,
        add_menu=add_menu,
        **kwargs,
    )


# pyvista/trame/views.py

"""Trame view interface for PyVista."""
import weakref

from trame.widgets.vtk import VtkLocalView, VtkRemoteLocalView, VtkRemoteView

CLOSED_PLOTTER_ERROR = "The render window for this plotter has been destroyed. Do not call `show()` for the plotter before passing to trame."


class _BasePyVistaView:
    def __init__(self, plotter):
        self._plotter = weakref.ref(plotter)
        self.pyvista_initialize()

    def pyvista_initialize(self):
        if self._plotter().render_window is None:
            raise RuntimeError(CLOSED_PLOTTER_ERROR)
        for renderer in self._plotter().renderers:
            if not renderer.camera.is_set:
                renderer.camera_position = renderer.get_default_cam_pos()
                renderer.ResetCamera()

    def _post_initialize(self):
        if self._server.running:
            self.update()
        else:
            self._server.controller.on_server_ready.add(self.update)

        # Callback to sync view on PyVista's render call when renders are suppressed
        self._plotter().add_on_render_callback(lambda *args: self.update(), render_event=False)

    def update_camera(self):
        """Update camera or push the image."""
        self.push_camera()
        self.update_image()


class PyVistaRemoteView(VtkRemoteView, _BasePyVistaView):
    """PyVista wrapping of trame ``VtkRemoteView`` for server rendering.

    This will connect to a PyVista plotter and stream the server-side
    renderings.

    Parameters
    ----------
    plotter : pyvista.Plotter
        The PyVista Plotter to display in the output view.

    interactive_ratio : int, optional
        Image size scale factor while interacting. Increasing this
        value will give higher resulotuion images during interaction
        events at the cost of performance. Use lower values (e.g.,
        ``0.5``) to increase performance while interacting.
        Defaults to 1.

    still_ratio : int, optional
        Image size scale factor while not interacting (still).
        Increasing this value will give higher resulotuion images
        when not interacting with the scene. Defaults to 1.

    namespace : str, optional
        The namespace for this view component. A default value is
        chosen based on the ``_id_name`` of the plotter.

    **kwargs : dict, optional
        Any additional keyword arguments to pass to
        ``trame.widgets.vtk.VtkRemoteView``.

    Notes
    -----
    For optimal rendering results, you may want to have the same
    value for ``interactive_ratio`` and ``still_ratio`` so that
    the entire rendering is not re-scaled between interaction events.

    """

    def __init__(self, plotter, interactive_ratio=None, still_ratio=None, namespace=None, **kwargs):
        """Create a trame remote view from a PyVista Plotter."""
        _BasePyVistaView.__init__(self, plotter)
        if namespace is None:
            namespace = f'{plotter._id_name}'
        if interactive_ratio is None:
            interactive_ratio = plotter._theme.trame.interactive_ratio
        if still_ratio is None:
            still_ratio = plotter._theme.trame.still_ratio
        VtkRemoteView.__init__(
            self,
            self._plotter().render_window,
            interactive_ratio=interactive_ratio,
            still_ratio=still_ratio,
            __properties=[('still_ratio', 'stillRatio')],
            ref=f'view_{plotter._id_name}',
            namespace=namespace,
            **kwargs,
        )
        self._post_initialize()

    def push_camera(self, *args, **kwargs):
        """No-op implementation to match local viewers."""
        pass  # pragma: no cover

    def update_image(self, *args, **kwargs):
        """Wrap update call."""
        return self.update(*args, **kwargs)


class PyVistaLocalView(VtkLocalView, _BasePyVistaView):
    """PyVista wrapping of trame VtkLocalView for in-browser rendering.

    This will connect to and synchronize with a PyVista plotter to
    perform client-side rendering with VTK.js in the browser.

    Parameters
    ----------
    plotter : pyvista.Plotter
        The PyVista Plotter to represent in the output view.

    namespace : str, optional
        The namespace for this view component. A default value is
        chosen based on the ``_id_name`` of the plotter.

    **kwargs : dict, optional
        Any additional keyword arguments to pass to
        ``trame.widgets.vtk.VtkLocalView``.

    """

    def __init__(self, plotter, namespace=None, **kwargs):
        """Create a trame local view from a PyVista Plotter."""
        _BasePyVistaView.__init__(self, plotter)
        if namespace is None:
            namespace = f'{plotter._id_name}'
        VtkLocalView.__init__(
            self,
            self._plotter().render_window,
            ref=f'view_{plotter._id_name}',
            namespace=namespace,
            **kwargs,
        )
        self._post_initialize()

    def update_image(self, *args, **kwargs):
        """No-op implementation to match remote viewers."""
        pass  # pragma: no cover


class PyVistaRemoteLocalView(VtkRemoteLocalView, _BasePyVistaView):
    """PyVista wrapping of trame ``VtkRemoteLocalView``.

    Dynamically switch between client and server rendering.

    Parameters
    ----------
    plotter : pyvista.Plotter
        The PyVista Plotter to display in the output view.

    interactive_ratio : int, optional
        Image size scale factor while interacting. Increasing this
        value will give higher resulotuion images during interaction
        events at the cost of performance. Use lower values (e.g.,
        ``0.5``) to increase performance while interacting.
        Defaults to 1. This is only valid in the ``'remote'`` mode.

    still_ratio : int, optional
        Image size scale factor while not interacting (still).
        Increasing this value will give higher resulotuion images
        when not interacting with the scene. Defaults to 1.
        This is only valid in the ``'remote'`` mode.

    namespace : str, optional
        The namespace for this view component. A default value is
        chosen based on the ``_id_name`` of the plotter.

    **kwargs : dict, optional
        Any additional keyword arguments to pass to
        ``trame.widgets.vtk.VtkRemoteLocalView``.

    """

    def __init__(self, plotter, interactive_ratio=None, still_ratio=None, namespace=None, **kwargs):
        """Create a trame remote/local view from a PyVista Plotter."""
        _BasePyVistaView.__init__(self, plotter)
        if namespace is None:
            namespace = f'{plotter._id_name}'
        if interactive_ratio is None:
            interactive_ratio = plotter._theme.trame.interactive_ratio
        if still_ratio is None:
            still_ratio = plotter._theme.trame.still_ratio
        VtkRemoteLocalView.__init__(
            self,
            self._plotter().render_window,
            interactive_ratio=interactive_ratio,
            still_ratio=still_ratio,
            __properties=[('still_ratio', 'stillRatio')],
            ref=f'view_{plotter._id_name}',
            namespace=namespace,
            **kwargs,
        )
        # Track namespace for our use since trame attributes are name mangled
        self._namespace = namespace

        self._post_initialize()


# pyvista/utilities/__init__.py

"""Utilities routines."""
from .algorithms import algorithm_to_mesh_handler, set_algorithm_input
from .common import perlin_noise, sample_function
from .errors import (
    GPUInfo,
    Observer,
    Report,
    assert_empty_kwargs,
    get_gpu_info,
    send_errors_to_logging,
    set_error_output_file,
    check_valid_vector,
    VtkErrorCatcher,
)
from .features import *
from .fileio import *
from .geometric_objects import *
from .helpers import *
from .parametric_objects import *
from .sphinx_gallery import Scraper, _get_sg_image_scraper
from .regression import compare_images
from . import transformations
from .xvfb import start_xvfb
from .reader import (
    get_reader,
    AVSucdReader,
    BaseReader,
    BinaryMarchingCubesReader,
    BMPReader,
    BYUReader,
    CGNSReader,
    DEMReader,
    EnSightReader,
    FacetReader,
    FluentReader,
    GIFReader,
    GLTFReader,
    HDFReader,
    HDRReader,
    JPEGReader,
    MetaImageReader,
    MFIXReader,
    MultiBlockPlot3DReader,
    NIFTIReader,
    NRRDReader,
    OBJReader,
    OpenFOAMReader,
    POpenFOAMReader,
    Plot3DFunctionEnum,
    Plot3DMetaReader,
    PLYReader,
    PNGReader,
    PNMReader,
    PointCellDataSelection,
    PTSReader,
    PVDDataSet,
    PVDReader,
    SegYReader,
    SLCReader,
    STLReader,
    TecplotReader,
    TIFFReader,
    TimeReader,
    VTKDataSetReader,
    VTKPDataSetReader,
    XMLImageDataReader,
    XMLPImageDataReader,
    XMLRectilinearGridReader,
    XMLPRectilinearGridReader,
    XMLUnstructuredGridReader,
    XMLPUnstructuredGridReader,
    XMLPolyDataReader,
    XMLStructuredGridReader,
    XMLMultiBlockDataReader,
    DICOMReader,
    XdmfReader,
)


# setup.py

"""See pyproject.toml for project metadata."""
from setuptools import setup

setup()


# pyvista/core/filters/__init__.py

"""These classes hold methods to apply general filters to any data type.

By inheriting these classes into the wrapped VTK data structures, a user
can easily apply common filters in an intuitive manner.

Example
-------
>>> import pyvista
>>> from pyvista import examples
>>> dataset = examples.load_uniform()

>>> # Threshold
>>> thresh = dataset.threshold([100, 500])

>>> # Slice
>>> slc = dataset.slice()

>>> # Clip
>>> clp = dataset.clip(invert=True)

>>> # Contour
>>> iso = dataset.contour()

"""

import pyvista
from pyvista.utilities import wrap, ProgressMonitor


def _update_alg(alg, progress_bar=False, message=''):
    """Update an algorithm with or without a progress bar."""
    if progress_bar:
        with ProgressMonitor(alg, message=message):
            alg.Update()
    else:
        alg.Update()


def _get_output(
    algorithm, iport=0, iconnection=0, oport=0, active_scalars=None, active_scalars_field='point'
):
    """Get the algorithm's output and copy input's pyvista meta info."""
    ido = wrap(algorithm.GetInputDataObject(iport, iconnection))
    data = wrap(algorithm.GetOutputDataObject(oport))
    if not isinstance(data, pyvista.MultiBlock):
        data.copy_meta_from(ido, deep=True)
        if not data.field_data and ido.field_data:
            data.field_data.update(ido.field_data)
        if active_scalars is not None:
            data.set_active_scalars(active_scalars, preference=active_scalars_field)
    # return a PointSet if input is a pointset
    if isinstance(ido, pyvista.PointSet):
        return data.cast_to_pointset()
    return data


# Re-export submodules to maintain the same import paths before filters.py was split into submodules
from .data_set import DataSetFilters
from .composite import CompositeFilters
from .poly_data import PolyDataFilters
from .unstructured_grid import UnstructuredGridFilters
from .rectilinear_grid import RectilinearGridFilters
from .structured_grid import StructuredGridFilters
from .uniform_grid import UniformGridFilters


__all__ = [
    '_update_alg',
    '_get_output',
    'CompositeFilters',
    'DataSetFilters',
    'PolyDataFilters',
    'RectilinearGridFilters',
    'StructuredGridFilters',
    'UniformGridFilters',
    'UnstructuredGridFilters',
]


# pyvista/core/filters/composite.py

"""Filters module with a class to manage filters/algorithms for composite datasets."""

import numpy as np

import pyvista
from pyvista import _vtk, abstract_class, wrap
from pyvista.core.filters import _get_output, _update_alg
from pyvista.core.filters.data_set import DataSetFilters


@abstract_class
class CompositeFilters:
    """An internal class to manage filters/algorithms for composite datasets."""

    def extract_geometry(self):
        """Extract the surface the geometry of all blocks.

        Place this filter at the end of a pipeline before a polydata
        consumer such as a polydata mapper to extract geometry from
        all blocks and append them to one polydata object.

        Returns
        -------
        pyvista.PolyData
            Surface of the composite dataset.

        """
        gf = _vtk.vtkCompositeDataGeometryFilter()
        gf.SetInputData(self)
        gf.Update()
        return wrap(gf.GetOutputDataObject(0))

    def combine(self, merge_points=False, tolerance=0.0):
        """Combine all blocks into a single unstructured grid.

        Parameters
        ----------
        merge_points : bool, default: False
            Merge coincidental points.

        tolerance : float, default: 0.0
            The absolute tolerance to use to find coincident points when
            ``merge_points=True``.

        Returns
        -------
        pyvista.UnstructuredGrid
            Combined blocks.

        Examples
        --------
        Combine blocks within a multiblock without merging points.

        >>> import pyvista
        >>> block = pyvista.MultiBlock(
        ...     [
        ...         pyvista.Cube(clean=False),
        ...         pyvista.Cube(center=(1, 0, 0), clean=False),
        ...     ]
        ... )
        >>> merged = block.combine()
        >>> merged.n_points
        48

        Combine blocks and merge points

        >>> merged = block.combine(merge_points=True)
        >>> merged.n_points
        12

        """
        alg = _vtk.vtkAppendFilter()
        for block in self:
            if isinstance(block, _vtk.vtkMultiBlockDataSet):
                block = CompositeFilters.combine(
                    block, merge_points=merge_points, tolerance=tolerance
                )
            alg.AddInputData(block)
        alg.SetMergePoints(merge_points)
        alg.SetTolerance(tolerance)
        alg.Update()
        return wrap(alg.GetOutputDataObject(0))

    clip = DataSetFilters.clip

    clip_box = DataSetFilters.clip_box

    slice = DataSetFilters.slice

    slice_orthogonal = DataSetFilters.slice_orthogonal

    slice_along_axis = DataSetFilters.slice_along_axis

    slice_along_line = DataSetFilters.slice_along_line

    extract_all_edges = DataSetFilters.extract_all_edges

    elevation = DataSetFilters.elevation

    compute_cell_sizes = DataSetFilters.compute_cell_sizes

    cell_centers = DataSetFilters.cell_centers

    cell_data_to_point_data = DataSetFilters.cell_data_to_point_data

    point_data_to_cell_data = DataSetFilters.point_data_to_cell_data

    triangulate = DataSetFilters.triangulate

    def outline(self, generate_faces=False, nested=False, progress_bar=False):
        """Produce an outline of the full extent for the all blocks in this composite dataset.

        Parameters
        ----------
        generate_faces : bool, default: False
            Generate solid faces for the box.

        nested : bool, default: False
            If ``True``, these creates individual outlines for each nested dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing the outline.

        """
        if nested:
            return DataSetFilters.outline(
                self, generate_faces=generate_faces, progress_bar=progress_bar
            )
        box = pyvista.Box(bounds=self.bounds)
        return box.outline(generate_faces=generate_faces, progress_bar=progress_bar)

    def outline_corners(self, factor=0.2, nested=False, progress_bar=False):
        """Produce an outline of the corners for the all blocks in this composite dataset.

        Parameters
        ----------
        factor : float, default: 0.2
            Controls the relative size of the corners to the length of
            the corresponding bounds.

        nested : bool, default: False
            If ``True``, these creates individual outlines for each nested dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing outlined corners.

        """
        if nested:
            return DataSetFilters.outline_corners(self, factor=factor, progress_bar=progress_bar)
        box = pyvista.Box(bounds=self.bounds)
        return box.outline_corners(factor=factor, progress_bar=progress_bar)

    def _compute_normals(
        self,
        cell_normals=True,
        point_normals=True,
        split_vertices=False,
        flip_normals=False,
        consistent_normals=True,
        auto_orient_normals=False,
        non_manifold_traversal=True,
        feature_angle=30.0,
        track_vertices=False,
        progress_bar=False,
    ):
        """Compute point and/or cell normals for a multi-block dataset."""
        if not self.is_all_polydata:
            raise RuntimeError(
                'This multiblock contains non-PolyData datasets. Convert all the '
                'datasets to PolyData with `as_polydata`'
            )

        # track original point indices
        if split_vertices and track_vertices:
            for block in self:
                ids = np.arange(block.n_points, dtype=pyvista.ID_TYPE)
                block.point_data.set_array(ids, 'pyvistaOriginalPointIds')

        alg = _vtk.vtkPolyDataNormals()
        alg.SetComputeCellNormals(cell_normals)
        alg.SetComputePointNormals(point_normals)
        alg.SetSplitting(split_vertices)
        alg.SetFlipNormals(flip_normals)
        alg.SetConsistency(consistent_normals)
        alg.SetAutoOrientNormals(auto_orient_normals)
        alg.SetNonManifoldTraversal(non_manifold_traversal)
        alg.SetFeatureAngle(feature_angle)
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Computing Normals')
        return _get_output(alg)


# pyvista/core/filters/data_set.py

"""Filters module with a class of common filters that can be applied to any vtkDataSet."""
import collections.abc
from typing import Optional, Sequence, Union
import warnings

import matplotlib.pyplot as plt
import numpy as np

import pyvista
from pyvista import FieldAssociation, _vtk
from pyvista.core.errors import VTKVersionError
from pyvista.core.filters import _get_output, _update_alg
from pyvista.errors import AmbiguousDataError, MissingDataError
from pyvista.utilities import (
    NORMALS,
    abstract_class,
    assert_empty_kwargs,
    generate_plane,
    get_array,
    get_array_association,
    transformations,
    wrap,
)
from pyvista.utilities.cells import numpy_to_idarr


@abstract_class
class DataSetFilters:
    """A set of common filters that can be applied to any vtkDataSet."""

    def _clip_with_function(
        self,
        function,
        invert=True,
        value=0.0,
        return_clipped=False,
        progress_bar=False,
        crinkle=False,
    ):
        """Clip using an implicit function (internal helper)."""
        if crinkle:
            # Add Cell IDs
            self.cell_data['cell_ids'] = np.arange(self.n_cells)

        if isinstance(self, _vtk.vtkPolyData):
            alg = _vtk.vtkClipPolyData()
        # elif isinstance(self, vtk.vtkImageData):
        #     alg = vtk.vtkClipVolume()
        #     alg.SetMixed3DCellGeneration(True)
        else:
            alg = _vtk.vtkTableBasedClipDataSet()
        alg.SetInputDataObject(self)  # Use the grid as the data we desire to cut
        alg.SetValue(value)
        alg.SetClipFunction(function)  # the implicit function
        alg.SetInsideOut(invert)  # invert the clip if needed
        alg.SetGenerateClippedOutput(return_clipped)
        _update_alg(alg, progress_bar, 'Clipping with Function')

        if return_clipped:
            a = _get_output(alg, oport=0)
            b = _get_output(alg, oport=1)
            if crinkle:
                a = self.extract_cells(np.unique(a.cell_data['cell_ids']))
                b = self.extract_cells(np.unique(b.cell_data['cell_ids']))
            return a, b
        clipped = _get_output(alg)
        if crinkle:
            clipped = self.extract_cells(np.unique(clipped.cell_data['cell_ids']))
        return clipped

    def align(
        self,
        target,
        max_landmarks=100,
        max_mean_distance=1e-5,
        max_iterations=500,
        check_mean_distance=True,
        start_by_matching_centroids=True,
        return_matrix=False,
    ):
        """Align a dataset to another.

        Uses the iterative closest point algorithm to align the points of the
        two meshes.  See the VTK class `vtkIterativeClosestPointTransform
        <https://vtk.org/doc/nightly/html/classvtkIterativeClosestPointTransform.html>`_

        Parameters
        ----------
        target : pyvista.DataSet
            The target dataset to align to.

        max_landmarks : int, default: 100
            The maximum number of landmarks.

        max_mean_distance : float, default: 1e-5
            The maximum mean distance for convergence.

        max_iterations : int, default: 500
            The maximum number of iterations.

        check_mean_distance : bool, default: True
            Whether to check the mean distance for convergence.

        start_by_matching_centroids : bool, default: True
            Whether to start the alignment by matching centroids. Default is True.

        return_matrix : bool, default: False
            Return the transform matrix as well as the aligned mesh.

        Returns
        -------
        aligned : pyvista.DataSet
            The dataset aligned to the target mesh.

        matrix : numpy.ndarray
            Transform matrix to transform the input dataset to the target dataset.

        Examples
        --------
        Create a cylinder, translate it, and use iterative closest point to
        align mesh to its original position.

        >>> import pyvista as pv
        >>> import numpy as np
        >>> source = pv.Cylinder(resolution=30).triangulate().subdivide(1)
        >>> transformed = source.rotate_y(20).translate([-0.75, -0.5, 0.5])
        >>> aligned = transformed.align(source)
        >>> _, closest_points = aligned.find_closest_cell(
        ...     source.points, return_closest_point=True
        ... )
        >>> dist = np.linalg.norm(source.points - closest_points, axis=1)

        Visualize the source, transformed, and aligned meshes.

        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_text('Before Alignment')
        >>> _ = pl.add_mesh(
        ...     source, style='wireframe', opacity=0.5, line_width=2
        ... )
        >>> _ = pl.add_mesh(transformed)
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_text('After Alignment')
        >>> _ = pl.add_mesh(
        ...     source, style='wireframe', opacity=0.5, line_width=2
        ... )
        >>> _ = pl.add_mesh(
        ...     aligned,
        ...     scalars=dist,
        ...     scalar_bar_args={
        ...         'title': 'Distance to Source',
        ...         'fmt': '%.1E',
        ...     },
        ... )
        >>> pl.show()

        Show that the mean distance between the source and the target is
        nearly zero.

        >>> np.abs(dist).mean()  # doctest:+SKIP
        9.997635192915073e-05

        """
        icp = _vtk.vtkIterativeClosestPointTransform()
        icp.SetSource(self)
        icp.SetTarget(target)
        icp.GetLandmarkTransform().SetModeToRigidBody()
        icp.SetMaximumNumberOfLandmarks(max_landmarks)
        icp.SetMaximumMeanDistance(max_mean_distance)
        icp.SetMaximumNumberOfIterations(max_iterations)
        icp.SetCheckMeanDistance(check_mean_distance)
        icp.SetStartByMatchingCentroids(start_by_matching_centroids)
        icp.Update()
        matrix = pyvista.array_from_vtkmatrix(icp.GetMatrix())
        if return_matrix:
            return self.transform(matrix, inplace=False), matrix
        return self.transform(matrix, inplace=False)

    def clip(
        self,
        normal='x',
        origin=None,
        invert=True,
        value=0.0,
        inplace=False,
        return_clipped=False,
        progress_bar=False,
        crinkle=False,
    ):
        """Clip a dataset by a plane by specifying the origin and normal.

        If no parameters are given the clip will occur in the center
        of that dataset.

        Parameters
        ----------
        normal : tuple(float) or str, default: 'x'
            Length 3 tuple for the normal vector direction. Can also
            be specified as a string conventional direction such as
            ``'x'`` for ``(1, 0, 0)`` or ``'-x'`` for ``(-1, 0, 0)``, etc.

        origin : sequence[float], optional
            The center ``(x, y, z)`` coordinate of the plane on which the clip
            occurs. The default is the center of the dataset.

        invert : bool, default: True
            Flag on whether to flip/invert the clip.

        value : float, default: 0.0
            Set the clipping value along the normal direction.

        inplace : bool, default: False
            Updates mesh in-place.

        return_clipped : bool, default: False
            Return both unclipped and clipped parts of the dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        crinkle : bool, default: False
            Crinkle the clip by extracting the entire cells along the
            clip. This adds the ``"cell_ids"`` array to the ``cell_data``
            attribute that tracks the original cell IDs of the original
            dataset.

        Returns
        -------
        pyvista.PolyData or tuple(pyvista.PolyData)
            Clipped mesh when ``return_clipped=False``,
            otherwise a tuple containing the unclipped and clipped datasets.

        Examples
        --------
        Clip a cube along the +X direction.  ``triangulate`` is used as
        the cube is initially composed of quadrilateral faces and
        subdivide only works on triangles.

        >>> import pyvista as pv
        >>> cube = pv.Cube().triangulate().subdivide(3)
        >>> clipped_cube = cube.clip()
        >>> clipped_cube.plot()

        Clip a cube in the +Z direction.  This leaves half a cube
        below the XY plane.

        >>> import pyvista as pv
        >>> cube = pv.Cube().triangulate().subdivide(3)
        >>> clipped_cube = cube.clip('z')
        >>> clipped_cube.plot()

        See :ref:`clip_with_surface_example` for more examples using this filter.

        """
        if isinstance(normal, str):
            normal = NORMALS[normal.lower()]
        # find center of data if origin not specified
        if origin is None:
            origin = self.center
        # create the plane for clipping
        function = generate_plane(normal, origin)
        # run the clip
        result = DataSetFilters._clip_with_function(
            self,
            function,
            invert=invert,
            value=value,
            return_clipped=return_clipped,
            progress_bar=progress_bar,
            crinkle=crinkle,
        )
        if inplace:
            if return_clipped:
                self.copy_from(result[0], deep=False)
                return self, result[1]
            else:
                self.copy_from(result, deep=False)
                return self
        return result

    def clip_box(
        self,
        bounds=None,
        invert=True,
        factor=0.35,
        progress_bar=False,
        merge_points=True,
        crinkle=False,
    ):
        """Clip a dataset by a bounding box defined by the bounds.

        If no bounds are given, a corner of the dataset bounds will be removed.

        Parameters
        ----------
        bounds : sequence[float], optional
            Length 6 sequence of floats: ``(xmin, xmax, ymin, ymax, zmin, zmax)``.
            Length 3 sequence of floats: distances from the min coordinate of
            of the input mesh. Single float value: uniform distance from the
            min coordinate. Length 12 sequence of length 3 sequence of floats:
            a plane collection (normal, center, ...).
            :class:`pyvista.PolyData`: if a poly mesh is passed that represents
            a box with 6 faces that all form a standard box, then planes will
            be extracted from the box to define the clipping region.

        invert : bool, default: True
            Flag on whether to flip/invert the clip.

        factor : float, default: 0.35
            If bounds are not given this is the factor along each axis to
            extract the default box.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        merge_points : bool, default: True
            If ``True``, coinciding points of independently defined mesh
            elements will be merged.

        crinkle : bool, default: False
            Crinkle the clip by extracting the entire cells along the
            clip. This adds the ``"cell_ids"`` array to the ``cell_data``
            attribute that tracks the original cell IDs of the original
            dataset.

        Returns
        -------
        pyvista.UnstructuredGrid
            Clipped dataset.

        Examples
        --------
        Clip a corner of a cube.  The bounds of a cube are normally
        ``[-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]``, and this removes 1/8 of
        the cube's surface.

        >>> import pyvista as pv
        >>> cube = pv.Cube().triangulate().subdivide(3)
        >>> clipped_cube = cube.clip_box([0, 1, 0, 1, 0, 1])
        >>> clipped_cube.plot()

        See :ref:`clip_with_plane_box_example` for more examples using this filter.

        """
        if bounds is None:

            def _get_quarter(dmin, dmax):
                """Get a section of the given range (internal helper)."""
                return dmax - ((dmax - dmin) * factor)

            xmin, xmax, ymin, ymax, zmin, zmax = self.bounds
            xmin = _get_quarter(xmin, xmax)
            ymin = _get_quarter(ymin, ymax)
            zmin = _get_quarter(zmin, zmax)
            bounds = [xmin, xmax, ymin, ymax, zmin, zmax]
        if isinstance(bounds, (float, int)):
            bounds = [bounds, bounds, bounds]
        elif isinstance(bounds, pyvista.PolyData):
            poly = bounds
            if poly.n_cells != 6:
                raise ValueError("The bounds mesh must have only 6 faces.")
            bounds = []
            poly.compute_normals(inplace=True)
            for cid in range(6):
                cell = poly.extract_cells(cid)
                normal = cell["Normals"][0]
                bounds.append(normal)
                bounds.append(cell.center)
        if not isinstance(bounds, (np.ndarray, collections.abc.Sequence)):
            raise TypeError('Bounds must be a sequence of floats with length 3, 6 or 12.')
        if len(bounds) not in [3, 6, 12]:
            raise ValueError('Bounds must be a sequence of floats with length 3, 6 or 12.')
        if len(bounds) == 3:
            xmin, xmax, ymin, ymax, zmin, zmax = self.bounds
            bounds = (xmin, xmin + bounds[0], ymin, ymin + bounds[1], zmin, zmin + bounds[2])
        if crinkle:
            self.cell_data['cell_ids'] = np.arange(self.n_cells)
        alg = _vtk.vtkBoxClipDataSet()
        if not merge_points:
            # vtkBoxClipDataSet uses vtkMergePoints by default
            alg.SetLocator(_vtk.vtkNonMergingPointLocator())
        alg.SetInputDataObject(self)
        alg.SetBoxClip(*bounds)
        port = 0
        if invert:
            # invert the clip if needed
            port = 1
            alg.GenerateClippedOutputOn()
        _update_alg(alg, progress_bar, 'Clipping a Dataset by a Bounding Box')
        clipped = _get_output(alg, oport=port)
        if crinkle:
            clipped = self.extract_cells(np.unique(clipped.cell_data['cell_ids']))
        return clipped

    def compute_implicit_distance(self, surface, inplace=False):
        """Compute the implicit distance from the points to a surface.

        This filter will compute the implicit distance from all of the
        nodes of this mesh to a given surface. This distance will be
        added as a point array called ``'implicit_distance'``.

        Parameters
        ----------
        surface : pyvista.DataSet
            The surface used to compute the distance.

        inplace : bool, default: False
            If ``True``, a new scalar array will be added to the
            ``point_data`` of this mesh and the modified mesh will
            be returned. Otherwise a copy of this mesh is returned
            with that scalar field added.

        Returns
        -------
        pyvista.DataSet
            Dataset containing the ``'implicit_distance'`` array in
            ``point_data``.

        Examples
        --------
        Compute the distance between all the points on a sphere and a
        plane.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> plane = pv.Plane()
        >>> _ = sphere.compute_implicit_distance(plane, inplace=True)
        >>> dist = sphere['implicit_distance']
        >>> type(dist)
        <class 'pyvista.core.pyvista_ndarray.pyvista_ndarray'>

        Plot these distances as a heatmap

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(
        ...     sphere, scalars='implicit_distance', cmap='bwr'
        ... )
        >>> _ = pl.add_mesh(plane, color='w', style='wireframe')
        >>> pl.show()

        See :ref:`clip_with_surface_example` and
        :ref:`voxelize_surface_mesh_example` for more examples using
        this filter.

        """
        function = _vtk.vtkImplicitPolyDataDistance()
        function.SetInput(surface)
        points = pyvista.convert_array(self.points)
        dists = _vtk.vtkDoubleArray()
        function.FunctionValue(points, dists)
        if inplace:
            self.point_data['implicit_distance'] = pyvista.convert_array(dists)
            return self
        result = self.copy()
        result.point_data['implicit_distance'] = pyvista.convert_array(dists)
        return result

    def clip_scalar(
        self, scalars=None, invert=True, value=0.0, inplace=False, progress_bar=False, both=False
    ):
        """Clip a dataset by a scalar.

        Parameters
        ----------
        scalars : str, optional
            Name of scalars to clip on.  Defaults to currently active scalars.

        invert : bool, default: True
            Flag on whether to flip/invert the clip.  When ``True``,
            only the mesh below ``value`` will be kept.  When
            ``False``, only values above ``value`` will be kept.

        value : float, default: 0.0
            Set the clipping value.

        inplace : bool, default: False
            Update mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        both : bool, default: False
            If ``True``, also returns the complementary clipped mesh.

        Returns
        -------
        pyvista.PolyData or tuple
            Clipped dataset if ``both=False``.  If ``both=True`` then
            returns a tuple of both clipped datasets.

        Examples
        --------
        Remove the part of the mesh with "sample_point_scalars" above 100.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> dataset = examples.load_hexbeam()
        >>> clipped = dataset.clip_scalar(
        ...     scalars="sample_point_scalars", value=100
        ... )
        >>> clipped.plot()

        Get clipped meshes corresponding to the portions of the mesh above and below 100.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> dataset = examples.load_hexbeam()
        >>> _below, _above = dataset.clip_scalar(
        ...     scalars="sample_point_scalars", value=100, both=True
        ... )

        Remove the part of the mesh with "sample_point_scalars" below 100.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> dataset = examples.load_hexbeam()
        >>> clipped = dataset.clip_scalar(
        ...     scalars="sample_point_scalars", value=100, invert=False
        ... )
        >>> clipped.plot()

        """
        if isinstance(self, _vtk.vtkPolyData):
            alg = _vtk.vtkClipPolyData()
        else:
            alg = _vtk.vtkTableBasedClipDataSet()

        alg.SetInputDataObject(self)
        alg.SetValue(value)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
        else:
            self.set_active_scalars(scalars)

        alg.SetInsideOut(invert)  # invert the clip if needed
        alg.SetGenerateClippedOutput(both)

        _update_alg(alg, progress_bar, 'Clipping by a Scalar')
        result0 = _get_output(alg)

        if inplace:
            self.copy_from(result0, deep=False)
            result0 = self

        if both:
            result1 = _get_output(alg, oport=1)
            if isinstance(self, _vtk.vtkPolyData):
                # For some reason vtkClipPolyData with SetGenerateClippedOutput on leaves unreferenced vertices
                result0, result1 = (r.clean() for r in (result0, result1))
            return result0, result1
        return result0

    def clip_surface(
        self,
        surface,
        invert=True,
        value=0.0,
        compute_distance=False,
        progress_bar=False,
        crinkle=False,
    ):
        """Clip any mesh type using a :class:`pyvista.PolyData` surface mesh.

        This will return a :class:`pyvista.UnstructuredGrid` of the clipped
        mesh. Geometry of the input dataset will be preserved where possible.
        Geometries near the clip intersection will be triangulated/tessellated.

        Parameters
        ----------
        surface : pyvista.PolyData
            The ``PolyData`` surface mesh to use as a clipping
            function.  If this input mesh is not a :class`pyvista.PolyData`,
            the external surface will be extracted.

        invert : bool, default: True
            Flag on whether to flip/invert the clip.

        value : float, default: 0.0
            Set the clipping value of the implicit function (if
            clipping with implicit function) or scalar value (if
            clipping with scalars).

        compute_distance : bool, default: False
            Compute the implicit distance from the mesh onto the input
            dataset.  A new array called ``'implicit_distance'`` will
            be added to the output clipped mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        crinkle : bool, default: False
            Crinkle the clip by extracting the entire cells along the
            clip. This adds the ``"cell_ids"`` array to the ``cell_data``
            attribute that tracks the original cell IDs of the original
            dataset.

        Returns
        -------
        pyvista.PolyData
            Clipped surface.

        Examples
        --------
        Clip a cube with a sphere.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(center=(-0.4, -0.4, -0.4))
        >>> cube = pyvista.Cube().triangulate().subdivide(3)
        >>> clipped = cube.clip_surface(sphere)
        >>> clipped.plot(show_edges=True, cpos='xy', line_width=3)

        See :ref:`clip_with_surface_example` for more examples using
        this filter.

        """
        if not isinstance(surface, _vtk.vtkPolyData):
            surface = DataSetFilters.extract_geometry(surface)
        function = _vtk.vtkImplicitPolyDataDistance()
        function.SetInput(surface)
        if compute_distance:
            points = pyvista.convert_array(self.points)
            dists = _vtk.vtkDoubleArray()
            function.FunctionValue(points, dists)
            self['implicit_distance'] = pyvista.convert_array(dists)
        # run the clip
        result = DataSetFilters._clip_with_function(
            self,
            function,
            invert=invert,
            value=value,
            progress_bar=progress_bar,
            crinkle=crinkle,
        )
        return result

    def slice_implicit(
        self, implicit_function, generate_triangles=False, contour=False, progress_bar=False
    ):
        """Slice a dataset by a VTK implicit function.

        Parameters
        ----------
        implicit_function : vtk.vtkImplicitFunction
            Specify the implicit function to perform the cutting.

        generate_triangles : bool, default: False
            If this is enabled (``False`` by default), the output will
            be triangles. Otherwise the output will be the intersection
            polygons. If the cutting function is not a plane, the
            output will be 3D polygons, which might be nice to look at
            but hard to compute with downstream.

        contour : bool, default: False
            If ``True``, apply a ``contour`` filter after slicing.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sliced dataset.

        Examples
        --------
        Slice the surface of a sphere.

        >>> import pyvista as pv
        >>> import vtk
        >>> sphere = vtk.vtkSphere()
        >>> sphere.SetRadius(10)
        >>> mesh = pv.Wavelet()
        >>> slice = mesh.slice_implicit(sphere)
        >>> slice.plot(show_edges=True, line_width=5)

        >>> sphere = vtk.vtkCylinder()
        >>> sphere.SetRadius(10)
        >>> mesh = pv.Wavelet()
        >>> slice = mesh.slice_implicit(sphere)
        >>> slice.plot(show_edges=True, line_width=5)

        """
        alg = _vtk.vtkCutter()  # Construct the cutter object
        alg.SetInputDataObject(self)  # Use the grid as the data we desire to cut
        alg.SetCutFunction(implicit_function)  # the cutter to use the function
        alg.SetGenerateTriangles(generate_triangles)
        _update_alg(alg, progress_bar, 'Slicing')
        output = _get_output(alg)
        if contour:
            return output.contour()
        return output

    def slice(
        self, normal='x', origin=None, generate_triangles=False, contour=False, progress_bar=False
    ):
        """Slice a dataset by a plane at the specified origin and normal vector orientation.

        If no origin is specified, the center of the input dataset will be used.

        Parameters
        ----------
        normal : sequence[float] | str, default: 'x'
            Length 3 tuple for the normal vector direction. Can also be
            specified as a string conventional direction such as ``'x'`` for
            ``(1, 0, 0)`` or ``'-x'`` for ``(-1, 0, 0)``, etc.

        origin : sequence[float], optional
            The center ``(x, y, z)`` coordinate of the plane on which
            the slice occurs.

        generate_triangles : bool, default: False
            If this is enabled (``False`` by default), the output will
            be triangles. Otherwise the output will be the intersection
            polygons.

        contour : bool, default: False
            If ``True``, apply a ``contour`` filter after slicing.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sliced dataset.

        Examples
        --------
        Slice the surface of a sphere.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> slice_x = sphere.slice(normal='x')
        >>> slice_y = sphere.slice(normal='y')
        >>> slice_z = sphere.slice(normal='z')
        >>> slices = slice_x + slice_y + slice_z
        >>> slices.plot(line_width=5)

        See :ref:`slice_example` for more examples using this filter.

        """
        if isinstance(normal, str):
            normal = NORMALS[normal.lower()]
        # find center of data if origin not specified
        if origin is None:
            origin = self.center
        # create the plane for clipping
        plane = generate_plane(normal, origin)
        return DataSetFilters.slice_implicit(
            self,
            plane,
            generate_triangles=generate_triangles,
            contour=contour,
            progress_bar=progress_bar,
        )

    def slice_orthogonal(
        self, x=None, y=None, z=None, generate_triangles=False, contour=False, progress_bar=False
    ):
        """Create three orthogonal slices through the dataset on the three cartesian planes.

        Yields a MutliBlock dataset of the three slices.

        Parameters
        ----------
        x : float, optional
            The X location of the YZ slice.

        y : float, optional
            The Y location of the XZ slice.

        z : float, optional
            The Z location of the XY slice.

        generate_triangles : bool, default: False
            When ``True``, the output will be triangles. Otherwise the output
            will be the intersection polygons.

        contour : bool, default: False
            If ``True``, apply a ``contour`` filter after slicing.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sliced dataset.

        Examples
        --------
        Slice the random hills dataset with three orthogonal planes.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> slices = hills.slice_orthogonal(contour=False)
        >>> slices.plot(line_width=5)

        See :ref:`slice_example` for more examples using this filter.

        """
        # Create the three slices
        if x is None:
            x = self.center[0]
        if y is None:
            y = self.center[1]
        if z is None:
            z = self.center[2]
        output = pyvista.MultiBlock()
        if isinstance(self, pyvista.MultiBlock):
            for i in range(self.n_blocks):
                output.append(
                    self[i].slice_orthogonal(
                        x=x, y=y, z=z, generate_triangles=generate_triangles, contour=contour
                    )
                )
            return output
        output.append(
            self.slice(
                normal='x',
                origin=[x, y, z],
                generate_triangles=generate_triangles,
                progress_bar=progress_bar,
            ),
            'YZ',
        )
        output.append(
            self.slice(
                normal='y',
                origin=[x, y, z],
                generate_triangles=generate_triangles,
                progress_bar=progress_bar,
            ),
            'XZ',
        )
        output.append(
            self.slice(
                normal='z',
                origin=[x, y, z],
                generate_triangles=generate_triangles,
                progress_bar=progress_bar,
            ),
            'XY',
        )
        return output

    def slice_along_axis(
        self,
        n=5,
        axis='x',
        tolerance=None,
        generate_triangles=False,
        contour=False,
        bounds=None,
        center=None,
        progress_bar=False,
    ):
        """Create many slices of the input dataset along a specified axis.

        Parameters
        ----------
        n : int, default: 5
            The number of slices to create.

        axis : str | int, default: 'x'
            The axis to generate the slices along. Perpendicular to the
            slices. Can be string name (``'x'``, ``'y'``, or ``'z'``) or
            axis index (``0``, ``1``, or ``2``).

        tolerance : float, optional
            The tolerance to the edge of the dataset bounds to create
            the slices. The ``n`` slices are placed equidistantly with
            an absolute padding of ``tolerance`` inside each side of the
            ``bounds`` along the specified axis. Defaults to 1% of the
            ``bounds`` along the specified axis.

        generate_triangles : bool, default: False
            When ``True``, the output will be triangles. Otherwise the output
            will be the intersection polygons.

        contour : bool, default: False
            If ``True``, apply a ``contour`` filter after slicing.

        bounds : sequence[float], optional
            A 6-length sequence overriding the bounds of the mesh.
            The bounds along the specified axis define the extent
            where slices are taken.

        center : sequence[float], optional
            A 3-length sequence specifying the position of the line
            along which slices are taken. Defaults to the center of
            the mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sliced dataset.

        Examples
        --------
        Slice the random hills dataset in the X direction.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> slices = hills.slice_along_axis(n=10)
        >>> slices.plot(line_width=5)

        Slice the random hills dataset in the Z direction.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> slices = hills.slice_along_axis(n=10, axis='z')
        >>> slices.plot(line_width=5)

        See :ref:`slice_example` for more examples using this filter.

        """
        # parse axis input
        labels = ['x', 'y', 'z']
        label_to_index = {label: index for index, label in enumerate(labels)}
        if isinstance(axis, int):
            ax_index = axis
            ax_label = labels[ax_index]
        elif isinstance(axis, str):
            try:
                ax_index = label_to_index[axis.lower()]
            except KeyError:
                raise ValueError(
                    f'Axis ({axis!r}) not understood. Choose one of {labels}.'
                ) from None
            ax_label = axis
        # get the locations along that axis
        if bounds is None:
            bounds = self.bounds
        if center is None:
            center = self.center
        if tolerance is None:
            tolerance = (bounds[ax_index * 2 + 1] - bounds[ax_index * 2]) * 0.01
        rng = np.linspace(bounds[ax_index * 2] + tolerance, bounds[ax_index * 2 + 1] - tolerance, n)
        center = list(center)
        # Make each of the slices
        output = pyvista.MultiBlock()
        if isinstance(self, pyvista.MultiBlock):
            for i in range(self.n_blocks):
                output.append(
                    self[i].slice_along_axis(
                        n=n,
                        axis=ax_label,
                        tolerance=tolerance,
                        generate_triangles=generate_triangles,
                        contour=contour,
                        bounds=bounds,
                        center=center,
                    )
                )
            return output
        for i in range(n):
            center[ax_index] = rng[i]
            slc = DataSetFilters.slice(
                self,
                normal=ax_label,
                origin=center,
                generate_triangles=generate_triangles,
                contour=contour,
                progress_bar=progress_bar,
            )
            output.append(slc, f'slice{i}')
        return output

    def slice_along_line(self, line, generate_triangles=False, contour=False, progress_bar=False):
        """Slice a dataset using a polyline/spline as the path.

        This also works for lines generated with :func:`pyvista.Line`.

        Parameters
        ----------
        line : pyvista.PolyData
            A PolyData object containing one single PolyLine cell.

        generate_triangles : bool, default: False
            When ``True``, the output will be triangles. Otherwise the output
            will be the intersection polygons.

        contour : bool, default: False
            If ``True``, apply a ``contour`` filter after slicing.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sliced dataset.

        Examples
        --------
        Slice the random hills dataset along a circular arc.

        >>> import numpy as np
        >>> import pyvista
        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> center = np.array(hills.center)
        >>> point_a = center + np.array([5, 0, 0])
        >>> point_b = center + np.array([-5, 0, 0])
        >>> arc = pyvista.CircularArc(
        ...     point_a, point_b, center, resolution=100
        ... )
        >>> line_slice = hills.slice_along_line(arc)

        Plot the circular arc and the hills mesh.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(hills, smooth_shading=True, style='wireframe')
        >>> _ = pl.add_mesh(
        ...     line_slice,
        ...     line_width=10,
        ...     render_lines_as_tubes=True,
        ...     color='k',
        ... )
        >>> _ = pl.add_mesh(arc, line_width=10, color='grey')
        >>> pl.show()

        See :ref:`slice_example` for more examples using this filter.

        """
        # check that we have a PolyLine cell in the input line
        if line.GetNumberOfCells() != 1:
            raise ValueError('Input line must have only one cell.')
        polyline = line.GetCell(0)
        if not isinstance(polyline, _vtk.vtkPolyLine):
            raise TypeError(f'Input line must have a PolyLine cell, not ({type(polyline)})')
        # Generate PolyPlane
        polyplane = _vtk.vtkPolyPlane()
        polyplane.SetPolyLine(polyline)
        # Create slice
        alg = _vtk.vtkCutter()  # Construct the cutter object
        alg.SetInputDataObject(self)  # Use the grid as the data we desire to cut
        alg.SetCutFunction(polyplane)  # the cutter to use the poly planes
        if not generate_triangles:
            alg.GenerateTrianglesOff()
        _update_alg(alg, progress_bar, 'Slicing along Line')
        output = _get_output(alg)
        if contour:
            return output.contour()
        return output

    def threshold(
        self,
        value=None,
        scalars=None,
        invert=False,
        continuous=False,
        preference='cell',
        all_scalars=False,
        component_mode='all',
        component=0,
        method='upper',
        progress_bar=False,
    ):
        """Apply a ``vtkThreshold`` filter to the input dataset.

        This filter will apply a ``vtkThreshold`` filter to the input
        dataset and return the resulting object. This extracts cells
        where the scalar value in each cell satisfies the threshold
        criterion.  If ``scalars`` is ``None``, the input's active
        scalars array is used.

        .. warning::
           Thresholding is inherently a cell operation, even though it can use
           associated point data for determining whether to keep a cell. In
           other words, whether or not a given point is included after
           thresholding depends on whether that point is part of a cell that
           is kept after thresholding.

           Please also note the default ``preference`` choice for CELL data
           over POINT data. This is contrary to most other places in PyVista's
           API where the preference typically defaults to POINT data. We chose
           to prefer CELL data here so that if thresholding by a named array
           that exists for both the POINT and CELL data, this filter will
           default to the CELL data array while performing the CELL-wise
           operation.

        Parameters
        ----------
        value : float | sequence[float], optional
            Single value or ``(min, max)`` to be used for the data threshold. If
            a sequence, then length must be 2. If no value is specified, the
            non-NaN data range will be used to remove any NaN values.
            Please reference the ``method`` parameter for how single values
            are handled.

        scalars : str, optional
            Name of scalars to threshold on. Defaults to currently active scalars.

        invert : bool, default: False
            Invert the threshold results. That is, cells that would have been
            in the output with this option off are excluded, while cells that
            would have been excluded from the output are included.

        continuous : bool, default: False
            When True, the continuous interval [minimum cell scalar,
            maximum cell scalar] will be used to intersect the threshold bound,
            rather than the set of discrete scalar values from the vertices.

        preference : str, default: 'cell'
            When ``scalars`` is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'`` or ``'cell'``. Throughout PyVista, the preference
            is typically ``'point'`` but since the threshold filter is a
            cell-wise operation, we prefer cell data for thresholding
            operations.

        all_scalars : bool, default: False
            If using scalars from point data, all
            points in a cell must satisfy the threshold when this
            value is ``True``.  When ``False``, any point of the cell
            with a scalar value satisfying the threshold criterion
            will extract the cell. Has no effect when using cell data.

        component_mode : {'selected', 'all', 'any'}
            The method to satisfy the criteria for the threshold of
            multicomponent scalars.  'selected' (default)
            uses only the ``component``.  'all' requires all
            components to meet criteria.  'any' is when
            any component satisfies the criteria.

        component : int, default: 0
            When using ``component_mode='selected'``, this sets
            which component to threshold on.

        method : str, default: 'upper'
            Set the threshold method for single-values, defining which
            threshold bounds to use. If the ``value`` is a range, this
            parameter will be ignored, extracting data between the two
            values. For single values, ``'lower'`` will extract data
            lower than the  ``value``. ``'upper'`` will extract data
            larger than the ``value``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Dataset containing geometry that meets the threshold requirements.

        Examples
        --------
        >>> import pyvista
        >>> import numpy as np
        >>> volume = np.zeros([10, 10, 10])
        >>> volume[:3] = 1
        >>> vol = pyvista.wrap(volume)
        >>> threshed = vol.threshold(0.1)
        >>> threshed
        UnstructuredGrid (...)
          N Cells:    243
          N Points:   400
          X Bounds:   0.000e+00, 3.000e+00
          Y Bounds:   0.000e+00, 9.000e+00
          Z Bounds:   0.000e+00, 9.000e+00
          N Arrays:   1

        Apply the threshold filter to Perlin noise.  First generate
        the structured grid.

        >>> import pyvista
        >>> noise = pyvista.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
        >>> grid = pyvista.sample_function(
        ...     noise, [0, 1.0, -0, 1.0, 0, 1.0], dim=(20, 20, 20)
        ... )
        >>> grid.plot(
        ...     cmap='gist_earth_r',
        ...     show_scalar_bar=True,
        ...     show_edges=False,
        ... )

        Next, apply the threshold.

        >>> import pyvista
        >>> noise = pyvista.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
        >>> grid = pyvista.sample_function(
        ...     noise, [0, 1.0, -0, 1.0, 0, 1.0], dim=(20, 20, 20)
        ... )
        >>> threshed = grid.threshold(value=0.02)
        >>> threshed.plot(
        ...     cmap='gist_earth_r',
        ...     show_scalar_bar=False,
        ...     show_edges=True,
        ... )

        See :ref:`common_filter_example` for more examples using this filter.

        """
        # set the scalars to threshold on
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            _, scalars = self.active_scalars_info
        arr = get_array(self, scalars, preference=preference, err=False)
        if arr is None:
            raise ValueError('No arrays present to threshold.')

        field = get_array_association(self, scalars, preference=preference)

        # Run a standard threshold algorithm
        alg = _vtk.vtkThreshold()
        alg.SetAllScalars(all_scalars)
        alg.SetInputDataObject(self)
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        # set thresholding parameters
        alg.SetUseContinuousCellRange(continuous)
        # use valid range if no value given
        if value is None:
            value = self.get_data_range(scalars)

        _set_threshold_limit(alg, value, method, invert)

        if component_mode == "component":
            alg.SetComponentModeToUseSelected()
            dim = arr.shape[1]
            if not isinstance(component, (int, np.integer)):
                raise TypeError("component must be int")
            if component > (dim - 1) or component < 0:
                raise ValueError(
                    f"scalars has {dim} components: supplied component {component} not in range"
                )
            alg.SetSelectedComponent(component)
        elif component_mode == "all":
            alg.SetComponentModeToUseAll()
        elif component_mode == "any":
            alg.SetComponentModeToUseAny()
        else:
            raise ValueError(
                f"component_mode must be 'component', 'all', or 'any' got: {component_mode}"
            )

        # Run the threshold
        _update_alg(alg, progress_bar, 'Thresholding')
        return _get_output(alg)

    def threshold_percent(
        self,
        percent=0.50,
        scalars=None,
        invert=False,
        continuous=False,
        preference='cell',
        method='upper',
        progress_bar=False,
    ):
        """Threshold the dataset by a percentage of its range on the active scalars array.

        .. warning::
           Thresholding is inherently a cell operation, even though it can use
           associated point data for determining whether to keep a cell. In
           other words, whether or not a given point is included after
           thresholding depends on whether that point is part of a cell that
           is kept after thresholding.

        Parameters
        ----------
        percent : float | sequence[float], optional
            The percentage in the range ``(0, 1)`` to threshold. If value is
            out of 0 to 1 range, then it will be divided by 100 and checked to
            be in that range.

        scalars : str, optional
            Name of scalars to threshold on. Defaults to currently active scalars.

        invert : bool, default: False
            Invert the threshold results. That is, cells that would have been
            in the output with this option off are excluded, while cells that
            would have been excluded from the output are included.

        continuous : bool, default: False
            When True, the continuous interval [minimum cell scalar,
            maximum cell scalar] will be used to intersect the threshold bound,
            rather than the set of discrete scalar values from the vertices.

        preference : str, default: 'cell'
            When ``scalars`` is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'`` or ``'cell'``. Throughout PyVista, the preference
            is typically ``'point'`` but since the threshold filter is a
            cell-wise operation, we prefer cell data for thresholding
            operations.

        method : str, default: 'upper'
            Set the threshold method for single-values, defining which
            threshold bounds to use. If the ``value`` is a range, this
            parameter will be ignored, extracting data between the two
            values. For single values, ``'lower'`` will extract data
            lower than the  ``value``. ``'upper'`` will extract data
            larger than the ``value``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Dataset containing geometry that meets the threshold requirements.

        Examples
        --------
        Apply a 50% threshold filter.

        >>> import pyvista
        >>> noise = pyvista.perlin_noise(0.1, (2, 2, 2), (0, 0, 0))
        >>> grid = pyvista.sample_function(
        ...     noise, [0, 1.0, -0, 1.0, 0, 1.0], dim=(30, 30, 30)
        ... )
        >>> threshed = grid.threshold_percent(0.5)
        >>> threshed.plot(
        ...     cmap='gist_earth_r',
        ...     show_scalar_bar=False,
        ...     show_edges=True,
        ... )

        Apply a 80% threshold filter.

        >>> threshed = grid.threshold_percent(0.8)
        >>> threshed.plot(
        ...     cmap='gist_earth_r',
        ...     show_scalar_bar=False,
        ...     show_edges=True,
        ... )

        See :ref:`common_filter_example` for more examples using a similar filter.

        """
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            _, tscalars = self.active_scalars_info
        else:
            tscalars = scalars
        dmin, dmax = self.get_data_range(arr_var=tscalars, preference=preference)

        def _check_percent(percent):
            """Make sure percent is between 0 and 1 or fix if between 0 and 100."""
            if percent >= 1:
                percent = float(percent) / 100.0
                if percent > 1:
                    raise ValueError(f'Percentage ({percent}) is out of range (0, 1).')
            if percent < 1e-10:
                raise ValueError(f'Percentage ({percent}) is too close to zero or negative.')
            return percent

        def _get_val(percent, dmin, dmax):
            """Get the value from a percentage of a range."""
            percent = _check_percent(percent)
            return dmin + float(percent) * (dmax - dmin)

        # Compute the values
        if isinstance(percent, (np.ndarray, collections.abc.Sequence)):
            # Get two values
            value = [_get_val(percent[0], dmin, dmax), _get_val(percent[1], dmin, dmax)]
        elif isinstance(percent, collections.abc.Iterable):
            raise TypeError('Percent must either be a single scalar or a sequence.')
        else:
            # Compute one value to threshold
            value = _get_val(percent, dmin, dmax)
        # Use the normal thresholding function on these values
        return DataSetFilters.threshold(
            self,
            value=value,
            scalars=scalars,
            invert=invert,
            continuous=continuous,
            preference=preference,
            method=method,
            progress_bar=progress_bar,
        )

    def outline(self, generate_faces=False, progress_bar=False):
        """Produce an outline of the full extent for the input dataset.

        Parameters
        ----------
        generate_faces : bool, default: False
            Generate solid faces for the box. This is disabled by default.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing an outline of the original dataset.

        Examples
        --------
        Generate and plot the outline of a sphere.  This is
        effectively the ``(x, y, z)`` bounds of the mesh.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> outline = sphere.outline()
        >>> pyvista.plot([sphere, outline], line_width=5)

        See :ref:`common_filter_example` for more examples using this filter.

        """
        alg = _vtk.vtkOutlineFilter()
        alg.SetInputDataObject(self)
        alg.SetGenerateFaces(generate_faces)
        _update_alg(alg, progress_bar, 'Producing an outline')
        return wrap(alg.GetOutputDataObject(0))

    def outline_corners(self, factor=0.2, progress_bar=False):
        """Produce an outline of the corners for the input dataset.

        Parameters
        ----------
        factor : float, default: 0.2
            Controls the relative size of the corners to the length of
            the corresponding bounds.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing outlined corners.

        Examples
        --------
        Generate and plot the corners of a sphere.  This is
        effectively the ``(x, y, z)`` bounds of the mesh.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> corners = sphere.outline_corners(factor=0.1)
        >>> pyvista.plot([sphere, corners], line_width=5)

        """
        alg = _vtk.vtkOutlineCornerFilter()
        alg.SetInputDataObject(self)
        alg.SetCornerFactor(factor)
        _update_alg(alg, progress_bar, 'Producing an Outline of the Corners')
        return wrap(alg.GetOutputDataObject(0))

    def extract_geometry(self, extent: Optional[Sequence[float]] = None, progress_bar=False):
        """Extract the outer surface of a volume or structured grid dataset.

        This will extract all 0D, 1D, and 2D cells producing the
        boundary faces of the dataset.

        .. note::
            This tends to be less efficient than :func:`extract_surface`.

        Parameters
        ----------
        extent : sequence[float], optional
            Specify a ``(xmin, xmax, ymin, ymax, zmin, zmax)`` bounding box to
            clip data.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Surface of the dataset.

        Examples
        --------
        Extract the surface of a sample unstructured grid.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> hex_beam.extract_geometry()
        PolyData (...)
          N Cells:    88
          N Points:   90
          N Strips:   0
          X Bounds:   0.000e+00, 1.000e+00
          Y Bounds:   0.000e+00, 1.000e+00
          Z Bounds:   0.000e+00, 5.000e+00
          N Arrays:   3

        See :ref:`surface_smoothing_example` for more examples using this filter.

        """
        alg = _vtk.vtkGeometryFilter()
        alg.SetInputDataObject(self)
        if extent is not None:
            alg.SetExtent(extent)
            alg.SetExtentClipping(True)
        _update_alg(alg, progress_bar, 'Extracting Geometry')
        return _get_output(alg)

    def extract_all_edges(self, use_all_points=False, clear_data=False, progress_bar=False):
        """Extract all the internal/external edges of the dataset as PolyData.

        This produces a full wireframe representation of the input dataset.

        Parameters
        ----------
        use_all_points : bool, default: False
            Indicates whether all of the points of the input mesh should exist
            in the output. When ``True``, point numbering does not change and
            a threaded approach is used, which avoids the use of a point locator
            and is quicker.

            By default this is set to ``False``, and unused points are omitted
            from the output.

            This parameter can only be set to ``True`` with ``vtk==9.1.0`` or newer.

        clear_data : bool, default: False
            Clear any point, cell, or field data. This is useful
            if wanting to strictly extract the edges.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Edges extracted from the dataset.

        Examples
        --------
        Extract the edges of a sample unstructured grid and plot the edges.
        Note how it plots interior edges.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> edges = hex_beam.extract_all_edges()
        >>> edges.plot(line_width=5, color='k')

        See :ref:`cell_centers_example` for more examples using this filter.

        """
        alg = _vtk.vtkExtractEdges()
        alg.SetInputDataObject(self)
        if use_all_points:
            try:
                alg.SetUseAllPoints(use_all_points)
            except AttributeError:  # pragma: no cover
                raise VTKVersionError(
                    'This version of VTK does not support `use_all_points=True`. '
                    'VTK v9.1 or newer is required.'
                )
        # vtkExtractEdges improperly uses INFO for debugging messages
        _vtk.vtkLogger.SetStderrVerbosity(_vtk.vtkLogger.VERBOSITY_OFF)
        _update_alg(alg, progress_bar, 'Extracting All Edges')
        # Reset vtkLogger to default verbosity level
        _vtk.vtkLogger.SetStderrVerbosity(_vtk.vtkLogger.VERBOSITY_INFO)
        output = _get_output(alg)
        if clear_data:
            output.clear_data()
        return output

    def elevation(
        self,
        low_point=None,
        high_point=None,
        scalar_range=None,
        preference='point',
        set_active=True,
        progress_bar=False,
    ):
        """Generate scalar values on a dataset.

        The scalar values lie within a user specified range, and are
        generated by computing a projection of each dataset point onto
        a line.  The line can be oriented arbitrarily.  A typical
        example is to generate scalars based on elevation or height
        above a plane.

        .. warning::
           This will create a scalars array named ``'Elevation'`` on the
           point data of the input dataset and overwrite the array
           named ``'Elevation'`` if present.

        Parameters
        ----------
        low_point : sequence[float], optional
            The low point of the projection line in 3D space. Default is bottom
            center of the dataset. Otherwise pass a length 3 sequence.

        high_point : sequence[float], optional
            The high point of the projection line in 3D space. Default is top
            center of the dataset. Otherwise pass a length 3 sequence.

        scalar_range : str | sequence[float], optional
            The scalar range to project to the low and high points on the line
            that will be mapped to the dataset. If None given, the values will
            be computed from the elevation (Z component) range between the
            high and low points. Min and max of a range can be given as a length
            2 sequence. If ``str``, name of scalar array present in the
            dataset given, the valid range of that array will be used.

        preference : str, default: "point"
            When an array name is specified for ``scalar_range``, this is the
            preferred array type to search for in the dataset.
            Must be either ``'point'`` or ``'cell'``.

        set_active : bool, default: True
            A boolean flag on whether or not to set the new
            ``'Elevation'`` scalar as the active scalars array on the
            output dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset containing elevation scalars in the
            ``"Elevation"`` array in ``point_data``.

        Examples
        --------
        Generate the "elevation" scalars for a sphere mesh.  This is
        simply the height in Z from the XY plane.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere_elv = sphere.elevation()
        >>> sphere_elv.plot(smooth_shading=True)

        Access the first 4 elevation scalars.  This is a point-wise
        array containing the "elevation" of each point.

        >>> sphere_elv['Elevation'][:4]  # doctest:+SKIP
        array([-0.5       ,  0.5       , -0.49706897, -0.48831028], dtype=float32)

        See :ref:`common_filter_example` for more examples using this filter.

        """
        # Fix the projection line:
        if low_point is None:
            low_point = list(self.center)
            low_point[2] = self.bounds[4]
        if high_point is None:
            high_point = list(self.center)
            high_point[2] = self.bounds[5]
        # Fix scalar_range:
        if scalar_range is None:
            scalar_range = (low_point[2], high_point[2])
        elif isinstance(scalar_range, str):
            scalar_range = self.get_data_range(arr_var=scalar_range, preference=preference)
        elif isinstance(scalar_range, (np.ndarray, collections.abc.Sequence)):
            if len(scalar_range) != 2:
                raise ValueError('scalar_range must have a length of two defining the min and max')
        else:
            raise TypeError(f'scalar_range argument ({scalar_range}) not understood.')
        # Construct the filter
        alg = _vtk.vtkElevationFilter()
        alg.SetInputDataObject(self)
        # Set the parameters
        alg.SetScalarRange(scalar_range)
        alg.SetLowPoint(low_point)
        alg.SetHighPoint(high_point)
        _update_alg(alg, progress_bar, 'Computing Elevation')
        # Decide on updating active scalars array
        output = _get_output(alg)
        if not set_active:
            # 'Elevation' is automatically made active by the VTK filter
            output.point_data.active_scalars_name = self.point_data.active_scalars_name
        return output

    def contour(
        self,
        isosurfaces=10,
        scalars=None,
        compute_normals=False,
        compute_gradients=False,
        compute_scalars=True,
        rng=None,
        preference='point',
        method='contour',
        progress_bar=False,
    ):
        """Contour an input self by an array.

        ``isosurfaces`` can be an integer specifying the number of
        isosurfaces in the data range or a sequence of values for
        explicitly setting the isosurfaces.

        Parameters
        ----------
        isosurfaces : int | sequence[float], optional
            Number of isosurfaces to compute across valid data range or a
            sequence of float values to explicitly use as the isosurfaces.

        scalars : str | array_like[float], optional
            Name or array of scalars to threshold on. If this is an array, the
            output of this filter will save them as ``"Contour Data"``.
            Defaults to currently active scalars.

        compute_normals : bool, default: False
            Compute normals for the dataset.

        compute_gradients : bool, default: False
            Compute gradients for the dataset.

        compute_scalars : bool, default: True
            Preserves the scalar values that are being contoured.

        rng : sequence[float], optional
            If an integer number of isosurfaces is specified, this is
            the range over which to generate contours. Default is the
            scalars array's full data range.

        preference : str, default: "point"
            When ``scalars`` is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'`` or ``'cell'``.

        method : str, default:  "contour"
            Specify to choose which vtk filter is used to create the contour.
            Must be one of ``'contour'``, ``'marching_cubes'`` and
            ``'flying_edges'``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Contoured surface.

        Examples
        --------
        Generate contours for the random hills dataset.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> contours = hills.contour()
        >>> contours.plot(line_width=5)

        Generate the surface of a mobius strip using flying edges.

        >>> import pyvista as pv
        >>> a = 0.4
        >>> b = 0.1
        >>> def f(x, y, z):
        ...     xx = x * x
        ...     yy = y * y
        ...     zz = z * z
        ...     xyz = x * y * z
        ...     xx_yy = xx + yy
        ...     a_xx = a * xx
        ...     b_yy = b * yy
        ...     return (
        ...         (xx_yy + 1) * (a_xx + b_yy)
        ...         + zz * (b * xx + a * yy)
        ...         - 2 * (a - b) * xyz
        ...         - a * b * xx_yy
        ...     ) ** 2 - 4 * (xx + yy) * (a_xx + b_yy - xyz * (a - b)) ** 2
        ...
        >>> n = 100
        >>> x_min, y_min, z_min = -1.35, -1.7, -0.65
        >>> grid = pv.UniformGrid(
        ...     dimensions=(n, n, n),
        ...     spacing=(
        ...         abs(x_min) / n * 2,
        ...         abs(y_min) / n * 2,
        ...         abs(z_min) / n * 2,
        ...     ),
        ...     origin=(x_min, y_min, z_min),
        ... )
        >>> x, y, z = grid.points.T
        >>> values = f(x, y, z)
        >>> out = grid.contour(
        ...     1,
        ...     scalars=values,
        ...     rng=[0, 0],
        ...     method='flying_edges',
        ... )
        >>> out.plot(color='tan', smooth_shading=True)

        See :ref:`common_filter_example` or
        :ref:`marching_cubes_example` for more examples using this
        filter.

        """
        if method is None or method == 'contour':
            alg = _vtk.vtkContourFilter()
        elif method == 'marching_cubes':
            alg = _vtk.vtkMarchingCubes()
        elif method == 'flying_edges':
            alg = _vtk.vtkFlyingEdges3D()
        else:
            raise ValueError(f"Method '{method}' is not supported")

        if rng is not None:
            if not isinstance(rng, (np.ndarray, collections.abc.Sequence)):
                raise TypeError(f'Array-like rng expected, got {type(rng).__name__}.')
            rng_shape = np.shape(rng)
            if rng_shape != (2,):
                raise ValueError(f'rng must be a two-length array-like, not {rng}.')
            if rng[0] > rng[1]:
                raise ValueError(f'rng must be a sorted min-max pair, not {rng}.')

        if isinstance(scalars, str):
            scalars_name = scalars
        elif isinstance(scalars, (collections.abc.Sequence, np.ndarray)):
            scalars_name = 'Contour Data'
            self[scalars_name] = scalars
        elif scalars is not None:
            raise TypeError(
                f'Invalid type for `scalars` ({type(scalars)}). Should be either '
                'a numpy.ndarray, a string, or None.'
            )

        # Make sure the input has scalars to contour on
        if self.n_arrays < 1:
            raise ValueError('Input dataset for the contour filter must have scalar.')

        alg.SetInputDataObject(self)
        alg.SetComputeNormals(compute_normals)
        alg.SetComputeGradients(compute_gradients)
        alg.SetComputeScalars(compute_scalars)
        # set the array to contour on
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars_name = self.active_scalars_info
        else:
            field = get_array_association(self, scalars_name, preference=preference)
        # NOTE: only point data is allowed? well cells works but seems buggy?
        if field != FieldAssociation.POINT:
            raise TypeError('Contour filter only works on point data.')
        alg.SetInputArrayToProcess(
            0,
            0,
            0,
            field.value,
            scalars_name,
        )  # args: (idx, port, connection, field, name)
        # set the isosurfaces
        if isinstance(isosurfaces, int):
            # generate values
            if rng is None:
                rng = self.get_data_range(scalars_name)
            alg.GenerateValues(isosurfaces, rng)
        elif isinstance(isosurfaces, (np.ndarray, collections.abc.Sequence)):
            alg.SetNumberOfContours(len(isosurfaces))
            for i, val in enumerate(isosurfaces):
                alg.SetValue(i, val)
        else:
            raise TypeError('isosurfaces not understood.')
        _update_alg(alg, progress_bar, 'Computing Contour')
        output = _get_output(alg)

        # some of these filters fail to correctly name the array
        if scalars_name not in output.point_data:
            if 'Unnamed_0' in output.point_data:
                output.point_data[scalars_name] = output.point_data.pop('Unnamed_0')

        return output

    def texture_map_to_plane(
        self,
        origin=None,
        point_u=None,
        point_v=None,
        inplace=False,
        name='Texture Coordinates',
        use_bounds=False,
        progress_bar=False,
    ):
        """Texture map this dataset to a user defined plane.

        This is often used to define a plane to texture map an image
        to this dataset.  The plane defines the spatial reference and
        extent of that image.

        Parameters
        ----------
        origin : sequence[float], optional
            Length 3 iterable of floats defining the XYZ coordinates of the
            bottom left corner of the plane.

        point_u : sequence[float], optional
            Length 3 iterable of floats defining the XYZ coordinates of the
            bottom right corner of the plane.

        point_v : sequence[float], optional
            Length 3 iterable of floats defining the XYZ coordinates of the
            top left corner of the plane.

        inplace : bool, default: False
            If ``True``, the new texture coordinates will be added to this
            dataset. If ``False``, a new dataset is returned with the texture
            coordinates.

        name : str, default: "Texture Coordinates"
            The string name to give the new texture coordinates if applying
            the filter inplace.

        use_bounds : bool, default: False
            Use the bounds to set the mapping plane by default (bottom plane
            of the bounding box).

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Original dataset with texture coordinates if
            ``inplace=True``, otherwise a copied dataset.

        Examples
        --------
        See :ref:`ref_topo_map_example`

        """
        if use_bounds:
            if isinstance(use_bounds, (int, bool)):
                b = self.GetBounds()
            origin = [b[0], b[2], b[4]]  # BOTTOM LEFT CORNER
            point_u = [b[1], b[2], b[4]]  # BOTTOM RIGHT CORNER
            point_v = [b[0], b[3], b[4]]  # TOP LEFT CORNER
        alg = _vtk.vtkTextureMapToPlane()
        if origin is None or point_u is None or point_v is None:
            alg.SetAutomaticPlaneGeneration(True)
        else:
            alg.SetOrigin(origin)  # BOTTOM LEFT CORNER
            alg.SetPoint1(point_u)  # BOTTOM RIGHT CORNER
            alg.SetPoint2(point_v)  # TOP LEFT CORNER
        alg.SetInputDataObject(self)
        _update_alg(alg, progress_bar, 'Texturing Map to Plane')
        output = _get_output(alg)
        if not inplace:
            return output
        t_coords = output.GetPointData().GetTCoords()
        t_coords.SetName(name)
        otc = self.GetPointData().GetTCoords()
        self.GetPointData().SetTCoords(t_coords)
        self.GetPointData().AddArray(t_coords)
        # CRITICAL:
        if otc and otc.GetName() != name:
            # Add old ones back at the end if different name
            self.GetPointData().AddArray(otc)
        return self

    def texture_map_to_sphere(
        self,
        center=None,
        prevent_seam=True,
        inplace=False,
        name='Texture Coordinates',
        progress_bar=False,
    ):
        """Texture map this dataset to a user defined sphere.

        This is often used to define a sphere to texture map an image
        to this dataset. The sphere defines the spatial reference and
        extent of that image.

        Parameters
        ----------
        center : sequence[float], optional
            Length 3 iterable of floats defining the XYZ coordinates of the
            center of the sphere. If ``None``, this will be automatically
            calculated.

        prevent_seam : bool, default: True
            Control how the texture coordinates are generated.  If
            set, the s-coordinate ranges from 0 to 1 and 1 to 0
            corresponding to the theta angle variation between 0 to
            180 and 180 to 0 degrees.  Otherwise, the s-coordinate
            ranges from 0 to 1 between 0 to 360 degrees.

        inplace : bool, default: False
            If ``True``, the new texture coordinates will be added to
            the dataset inplace. If ``False`` (default), a new dataset
            is returned with the texture coordinates.

        name : str, default: "Texture Coordinates"
            The string name to give the new texture coordinates if applying
            the filter inplace.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset containing the texture mapped to a sphere.  Return
            type matches input.

        Examples
        --------
        See :ref:`ref_texture_example`.

        """
        alg = _vtk.vtkTextureMapToSphere()
        if center is None:
            alg.SetAutomaticSphereGeneration(True)
        else:
            alg.SetAutomaticSphereGeneration(False)
            alg.SetCenter(center)
        alg.SetPreventSeam(prevent_seam)
        alg.SetInputDataObject(self)
        _update_alg(alg, progress_bar, 'Maping texture to sphere')
        output = _get_output(alg)
        if not inplace:
            return output
        t_coords = output.GetPointData().GetTCoords()
        t_coords.SetName(name)
        otc = self.GetPointData().GetTCoords()
        self.GetPointData().SetTCoords(t_coords)
        self.GetPointData().AddArray(t_coords)
        # CRITICAL:
        if otc and otc.GetName() != name:
            # Add old ones back at the end if different name
            self.GetPointData().AddArray(otc)
        return self

    def compute_cell_sizes(self, length=True, area=True, volume=True, progress_bar=False):
        """Compute sizes for 1D (length), 2D (area) and 3D (volume) cells.

        Parameters
        ----------
        length : bool, default: True
            Specify whether or not to compute the length of 1D cells.

        area : bool, default: True
            Specify whether or not to compute the area of 2D cells.

        volume : bool, default: True
            Specify whether or not to compute the volume of 3D cells.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with `cell_data` containing the ``"Length"``,
            ``"Area"``, and ``"Volume"`` arrays if set in the
            parameters.  Return type matches input.

        Notes
        -----
        If cells do not have a dimension (for example, the length of
        hexahedral cells), the corresponding array will be all zeros.

        Examples
        --------
        Compute the face area of the example airplane mesh.

        >>> from pyvista import examples
        >>> surf = examples.load_airplane()
        >>> surf = surf.compute_cell_sizes(length=False, volume=False)
        >>> surf.plot(show_edges=True, scalars='Area')

        """
        alg = _vtk.vtkCellSizeFilter()
        alg.SetInputDataObject(self)
        alg.SetComputeArea(area)
        alg.SetComputeVolume(volume)
        alg.SetComputeLength(length)
        alg.SetComputeVertexCount(False)
        _update_alg(alg, progress_bar, 'Computing Cell Sizes')
        return _get_output(alg)

    def cell_centers(self, vertex=True, progress_bar=False):
        """Generate points at the center of the cells in this dataset.

        These points can be used for placing glyphs or vectors.

        Parameters
        ----------
        vertex : bool, default: True
            Enable or disable the generation of vertex cells.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Polydata where the points are the cell centers of the
            original dataset.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Plane()
        >>> mesh.point_data.clear()
        >>> centers = mesh.cell_centers()
        >>> pl = pyvista.Plotter()
        >>> actor = pl.add_mesh(mesh, show_edges=True)
        >>> actor = pl.add_points(
        ...     centers,
        ...     render_points_as_spheres=True,
        ...     color='red',
        ...     point_size=20,
        ... )
        >>> pl.show()

        See :ref:`cell_centers_example` for more examples using this filter.

        """
        alg = _vtk.vtkCellCenters()
        alg.SetInputDataObject(self)
        alg.SetVertexCells(vertex)
        _update_alg(alg, progress_bar, 'Generating Points at the Center of the Cells')
        return _get_output(alg)

    def glyph(
        self,
        orient=True,
        scale=True,
        factor=1.0,
        geom=None,
        indices=None,
        tolerance=None,
        absolute=False,
        clamping=False,
        rng=None,
        progress_bar=False,
    ):
        """Copy a geometric representation (called a glyph) to the input dataset.

        The glyph may be oriented along the input vectors, and it may
        be scaled according to scalar data or vector
        magnitude. Passing a table of glyphs to choose from based on
        scalars or vector magnitudes is also supported.  The arrays
        used for ``orient`` and ``scale`` must be either both point data
        or both cell data.

        Parameters
        ----------
        orient : bool | str, default: True
            If ``True``, use the active vectors array to orient the glyphs.
            If string, the vector array to use to orient the glyphs.
            If ``False``, the glyphs will not be orientated.

        scale : bool | str | sequence[float], default: True
            If ``True``, use the active scalars to scale the glyphs.
            If string, the scalar array to use to scale the glyphs.
            If ``False``, the glyphs will not be scaled.

        factor : float, default: 1.0
            Scale factor applied to scaling array.

        geom : vtk.vtkDataSet or tuple(vtk.vtkDataSet), optional
            The geometry to use for the glyph. If missing, an arrow glyph
            is used. If a sequence, the datasets inside define a table of
            geometries to choose from based on scalars or vectors. In this
            case a sequence of numbers of the same length must be passed as
            ``indices``. The values of the range (see ``rng``) affect lookup
            in the table.

        indices : sequence[float], optional
            Specifies the index of each glyph in the table for lookup in case
            ``geom`` is a sequence. If given, must be the same length as
            ``geom``. If missing, a default value of ``range(len(geom))`` is
            used. Indices are interpreted in terms of the scalar range
            (see ``rng``). Ignored if ``geom`` has length 1.

        tolerance : float, optional
            Specify tolerance in terms of fraction of bounding box length.
            Float value is between 0 and 1. Default is None. If ``absolute``
            is ``True`` then the tolerance can be an absolute distance.
            If ``None``, points merging as a preprocessing step is disabled.

        absolute : bool, default: False
            Control if ``tolerance`` is an absolute distance or a fraction.

        clamping : bool, default: False
            Turn on/off clamping of "scalar" values to range.

        rng : sequence[float], optional
            Set the range of values to be considered by the filter
            when scalars values are provided.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Glyphs at either the cell centers or points.

        Examples
        --------
        Create arrow glyphs oriented by vectors and scaled by scalars.
        Factor parameter is used to reduce the size of the arrows.

        >>> import pyvista
        >>> from pyvista import examples
        >>> mesh = examples.load_random_hills()
        >>> arrows = mesh.glyph(
        ...     scale="Normals", orient="Normals", tolerance=0.05
        ... )
        >>> pl = pyvista.Plotter()
        >>> actor = pl.add_mesh(arrows, color="black")
        >>> actor = pl.add_mesh(
        ...     mesh,
        ...     scalars="Elevation",
        ...     cmap="terrain",
        ...     show_scalar_bar=False,
        ... )
        >>> pl.show()

        See :ref:`glyph_example` and :ref:`glyph_table_example` for more
        examples using this filter.

        """
        dataset = self

        # Make glyphing geometry if necessary
        if geom is None:
            arrow = _vtk.vtkArrowSource()
            _update_alg(arrow, progress_bar, 'Making Arrow')
            geom = arrow.GetOutput()
        # Check if a table of geometries was passed
        if isinstance(geom, (np.ndarray, collections.abc.Sequence)):
            if indices is None:
                # use default "categorical" indices
                indices = np.arange(len(geom))
            if not isinstance(indices, (np.ndarray, collections.abc.Sequence)):
                raise TypeError(
                    'If "geom" is a sequence then "indices" must '
                    'also be a sequence of the same length.'
                )
            if len(indices) != len(geom) and len(geom) != 1:
                raise ValueError('The sequence "indices" must be the same length ' 'as "geom".')
        else:
            geom = [geom]
        if any(not isinstance(subgeom, _vtk.vtkPolyData) for subgeom in geom):
            raise TypeError('Only PolyData objects can be used as glyphs.')
        # Run the algorithm
        alg = _vtk.vtkGlyph3D()
        if len(geom) == 1:
            # use a single glyph, ignore indices
            alg.SetSourceData(geom[0])
        else:
            for index, subgeom in zip(indices, geom):
                alg.SetSourceData(index, subgeom)
            if dataset.active_scalars is not None:
                if dataset.active_scalars.ndim > 1:
                    alg.SetIndexModeToVector()
                else:
                    alg.SetIndexModeToScalar()
            else:
                alg.SetIndexModeToOff()

        if isinstance(scale, str):
            dataset.set_active_scalars(scale, preference='cell')
            scale = True
        elif isinstance(scale, bool) and scale:
            try:
                pyvista.set_default_active_scalars(self)
            except MissingDataError:
                warnings.warn("No data to use for scale. scale will be set to False.")
                scale = False
            except AmbiguousDataError as err:
                warnings.warn(f"{err}\nIt is unclear which one to use. scale will be set to False.")
                scale = False

        if scale:
            if dataset.active_scalars is not None:
                if dataset.active_scalars.ndim > 1:
                    alg.SetScaleModeToScaleByVector()
                else:
                    alg.SetScaleModeToScaleByScalar()
        else:
            alg.SetScaleModeToDataScalingOff()

        if isinstance(orient, str):
            if scale and dataset.active_scalars_info.association == FieldAssociation.CELL:
                prefer = 'cell'
            else:
                prefer = 'point'
            dataset.set_active_vectors(orient, preference=prefer)
            orient = True

        if orient:
            try:
                pyvista.set_default_active_vectors(dataset)
            except MissingDataError:
                warnings.warn("No vector-like data to use for orient. orient will be set to False.")
                orient = False
            except AmbiguousDataError as err:
                warnings.warn(
                    f"{err}\nIt is unclear which one to use. orient will be set to False."
                )
                orient = False

        if scale and orient:
            if dataset.active_vectors_info.association != dataset.active_scalars_info.association:
                raise ValueError("Both ``scale`` and ``orient`` must use point data or cell data.")

        source_data = dataset
        set_actives_on_source_data = False

        if (scale and dataset.active_scalars_info.association == FieldAssociation.CELL) or (
            orient and dataset.active_vectors_info.association == FieldAssociation.CELL
        ):
            source_data = dataset.cell_centers()
            set_actives_on_source_data = True

        # Clean the points before glyphing
        if tolerance is not None:
            small = pyvista.PolyData(source_data.points)
            small.point_data.update(source_data.point_data)
            source_data = small.clean(
                point_merging=True,
                merge_tol=tolerance,
                lines_to_points=False,
                polys_to_lines=False,
                strips_to_polys=False,
                inplace=False,
                absolute=absolute,
                progress_bar=progress_bar,
            )
            set_actives_on_source_data = True

        # upstream operations (cell to point conversion, point merging) may have unset the correct active
        # scalars/vectors, so set them again
        if set_actives_on_source_data:
            if scale:
                source_data.set_active_scalars(dataset.active_scalars_name, preference='point')
            if orient:
                source_data.set_active_vectors(dataset.active_vectors_name, preference='point')

        if rng is not None:
            alg.SetRange(rng)
        alg.SetOrient(orient)
        alg.SetInputData(source_data)
        alg.SetVectorModeToUseVector()
        alg.SetScaleFactor(factor)
        alg.SetClamping(clamping)
        _update_alg(alg, progress_bar, 'Computing Glyphs')
        return _get_output(alg)

    def connectivity(self, largest=False, progress_bar=False):
        """Find and label connected bodies/volumes.

        This adds an ID array to the point and cell data to
        distinguish separate connected bodies. This applies a
        ``vtkConnectivityFilter`` filter which extracts cells that
        share common points and/or meet other connectivity criterion.

        Cells that share vertices and meet other connectivity
        criterion such as scalar range are known as a region.

        Parameters
        ----------
        largest : bool, default: False
            Extract the largest connected part of the mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with labeled connected bodies.  Return type
            matches input.

        Examples
        --------
        Join two meshes together and plot their connectivity.

        >>> import pyvista
        >>> mesh = pyvista.Sphere() + pyvista.Sphere(center=(2, 0, 0))
        >>> conn = mesh.connectivity(largest=False)
        >>> conn.plot(cmap=['red', 'blue'])

        See :ref:`volumetric_example` for more examples using this filter.

        """
        alg = _vtk.vtkConnectivityFilter()
        alg.SetInputData(self)
        if largest:
            alg.SetExtractionModeToLargestRegion()
        else:
            alg.SetExtractionModeToAllRegions()
        alg.SetColorRegions(True)
        _update_alg(alg, progress_bar, 'Finding and Labeling Connected Bodies/Volumes.')
        output = _get_output(alg)

        # remove regionID from the output when extraction mode is set to the
        # largest to avoid the VTK warning:
        # the Cell array RegionId ... has X tuples but there are only Y cells
        if largest:
            output.cell_data.pop('RegionId', None)
        return output

    def extract_largest(self, inplace=False, progress_bar=False):
        """Extract largest connected set in mesh.

        Can be used to reduce residues obtained when generating an
        isosurface.  Works only if residues are not connected (share
        at least one point with) the main component of the image.

        Parameters
        ----------
        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Largest connected set in the dataset.  Return type matches input.

        Examples
        --------
        Join two meshes together, extract the largest, and plot it.

        >>> import pyvista
        >>> mesh = pyvista.Sphere() + pyvista.Cube()
        >>> largest = mesh.extract_largest()
        >>> largest.point_data.clear()
        >>> largest.cell_data.clear()
        >>> largest.plot()

        See :ref:`volumetric_example` for more examples using this filter.

        """
        mesh = DataSetFilters.connectivity(self, largest=True, progress_bar=progress_bar)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def split_bodies(self, label=False, progress_bar=False):
        """Find, label, and split connected bodies/volumes.

        This splits different connected bodies into blocks in a
        :class:`pyvista.MultiBlock` dataset.

        Parameters
        ----------
        label : bool, default: False
            A flag on whether to keep the ID arrays given by the
            ``connectivity`` filter.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.MultiBlock
            MultiBlock with a split bodies.

        Examples
        --------
        Split a uniform grid thresholded to be non-connected.

        >>> from pyvista import examples
        >>> dataset = examples.load_uniform()
        >>> _ = dataset.set_active_scalars('Spatial Cell Data')
        >>> threshed = dataset.threshold_percent([0.15, 0.50], invert=True)
        >>> bodies = threshed.split_bodies()
        >>> len(bodies)
        2

        See :ref:`split_vol_ref` for more examples using this filter.

        """
        # Get the connectivity and label different bodies
        labeled = DataSetFilters.connectivity(self)
        classifier = labeled.cell_data['RegionId']
        bodies = pyvista.MultiBlock()
        for vid in np.unique(classifier):
            # Now extract it:
            b = labeled.threshold(
                [vid - 0.5, vid + 0.5], scalars='RegionId', progress_bar=progress_bar
            )
            if not label:
                # strange behavior:
                # must use this method rather than deleting from the point_data
                # or else object is collected.
                b.cell_data.remove('RegionId')
                b.point_data.remove('RegionId')
            bodies.append(b)

        return bodies

    def warp_by_scalar(
        self, scalars=None, factor=1.0, normal=None, inplace=False, progress_bar=False, **kwargs
    ):
        """Warp the dataset's points by a point data scalars array's values.

        This modifies point coordinates by moving points along point
        normals by the scalar amount times the scale factor.

        Parameters
        ----------
        scalars : str, optional
            Name of scalars to warp by. Defaults to currently active scalars.

        factor : float, default: 1.0
            A scaling factor to increase the scaling effect. Alias
            ``scale_factor`` also accepted - if present, overrides ``factor``.

        normal : sequence, optional
            User specified normal. If given, data normals will be
            ignored and the given normal will be used to project the
            warp.

        inplace : bool, default: False
            If ``True``, the points of the given dataset will be updated.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            Accepts ``scale_factor`` instead of ``factor``.

        Returns
        -------
        pyvista.DataSet
            Warped Dataset.  Return type matches input.

        Examples
        --------
        First, plot the unwarped mesh.

        >>> from pyvista import examples
        >>> mesh = examples.download_st_helens()
        >>> mesh.plot(cmap='gist_earth', show_scalar_bar=False)

        Now, warp the mesh by the ``'Elevation'`` scalars.

        >>> warped = mesh.warp_by_scalar('Elevation')
        >>> warped.plot(cmap='gist_earth', show_scalar_bar=False)

        See :ref:`surface_normal_example` for more examples using this filter.

        """
        factor = kwargs.pop('scale_factor', factor)
        assert_empty_kwargs(**kwargs)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        _ = get_array(self, scalars, preference='point', err=True)

        field = get_array_association(self, scalars, preference='point')
        if field != FieldAssociation.POINT:
            raise TypeError('Dataset can only by warped by a point data array.')
        # Run the algorithm
        alg = _vtk.vtkWarpScalar()
        alg.SetInputDataObject(self)
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        alg.SetScaleFactor(factor)
        if normal is not None:
            alg.SetNormal(normal)
            alg.SetUseNormal(True)
        _update_alg(alg, progress_bar, 'Warping by Scalar')
        output = _get_output(alg)
        if inplace:
            if isinstance(self, (_vtk.vtkImageData, _vtk.vtkRectilinearGrid)):
                raise TypeError("This filter cannot be applied inplace for this mesh type.")
            self.copy_from(output, deep=False)
            return self
        return output

    def warp_by_vector(self, vectors=None, factor=1.0, inplace=False, progress_bar=False):
        """Warp the dataset's points by a point data vectors array's values.

        This modifies point coordinates by moving points along point
        vectors by the local vector times the scale factor.

        A classical application of this transform is to visualize
        eigenmodes in mechanics.

        Parameters
        ----------
        vectors : str, optional
            Name of vector to warp by. Defaults to currently active vector.

        factor : float, default: 1.0
            A scaling factor that multiplies the vectors to warp by. Can
            be used to enhance the warping effect.

        inplace : bool, default: False
            If ``True``, the function will update the mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The warped mesh resulting from the operation.

        Examples
        --------
        Warp a sphere by vectors.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> sphere = examples.load_sphere_vectors()
        >>> warped = sphere.warp_by_vector()
        >>> pl = pv.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> actor = pl.add_text("Before warp")
        >>> actor = pl.add_mesh(sphere, color='white')
        >>> pl.subplot(0, 1)
        >>> actor = pl.add_text("After warp")
        >>> actor = pl.add_mesh(warped, color='white')
        >>> pl.show()

        See :ref:`warp_by_vectors_example` and :ref:`eigenmodes_example` for
        more examples using this filter.

        """
        if vectors is None:
            pyvista.set_default_active_vectors(self)
            field, vectors = self.active_vectors_info
        arr = get_array(self, vectors, preference='point')
        field = get_array_association(self, vectors, preference='point')
        if arr is None:
            raise ValueError('No vectors present to warp by vector.')

        # check that this is indeed a vector field
        if arr.ndim != 2 or arr.shape[1] != 3:
            raise ValueError(
                'Dataset can only by warped by a 3D vector point data array. '
                'The values you provided do not satisfy this requirement'
            )
        alg = _vtk.vtkWarpVector()
        alg.SetInputDataObject(self)
        alg.SetInputArrayToProcess(0, 0, 0, field.value, vectors)
        alg.SetScaleFactor(factor)
        _update_alg(alg, progress_bar, 'Warping by Vector')
        warped_mesh = _get_output(alg)
        if inplace:
            self.copy_from(warped_mesh, deep=False)
            return self
        else:
            return warped_mesh

    def cell_data_to_point_data(self, pass_cell_data=False, progress_bar=False):
        """Transform cell data into point data.

        Point data are specified per node and cell data specified
        within cells.  Optionally, the input point data can be passed
        through to the output.

        The method of transformation is based on averaging the data
        values of all cells using a particular point. Optionally, the
        input cell data can be passed through to the output as well.

        See also :func:`pyvista.DataSetFilters.point_data_to_cell_data`.

        Parameters
        ----------
        pass_cell_data : bool, default: False
            If enabled, pass the input cell data through to the output.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with the point data transformed into cell data.
            Return type matches input.

        Examples
        --------
        First compute the face area of the example airplane mesh and
        show the cell values.  This is to show discrete cell data.

        >>> from pyvista import examples
        >>> surf = examples.load_airplane()
        >>> surf = surf.compute_cell_sizes(length=False, volume=False)
        >>> surf.plot(scalars='Area')

        These cell scalars can be applied to individual points to
        effectively smooth out the cell data onto the points.

        >>> from pyvista import examples
        >>> surf = examples.load_airplane()
        >>> surf = surf.compute_cell_sizes(length=False, volume=False)
        >>> surf = surf.cell_data_to_point_data()
        >>> surf.plot(scalars='Area')

        """
        alg = _vtk.vtkCellDataToPointData()
        alg.SetInputDataObject(self)
        alg.SetPassCellData(pass_cell_data)
        _update_alg(alg, progress_bar, 'Transforming cell data into point data.')
        active_scalars = None
        if not isinstance(self, pyvista.MultiBlock):
            active_scalars = self.active_scalars_name
        return _get_output(alg, active_scalars=active_scalars)

    def ctp(self, pass_cell_data=False, progress_bar=False, **kwargs):
        """Transform cell data into point data.

        Point data are specified per node and cell data specified
        within cells.  Optionally, the input point data can be passed
        through to the output.

        This method is an alias for
        :func:`pyvista.DataSetFilters.cell_data_to_point_data`.

        Parameters
        ----------
        pass_cell_data : bool, default: False
            If enabled, pass the input cell data through to the output.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            Depreciated keyword argument ``pass_cell_arrays``.

        Returns
        -------
        pyvista.DataSet
            Dataset with the cell data transformed into point data.
            Return type matches input.

        """
        return DataSetFilters.cell_data_to_point_data(
            self, pass_cell_data=pass_cell_data, progress_bar=progress_bar, **kwargs
        )

    def point_data_to_cell_data(self, pass_point_data=False, progress_bar=False):
        """Transform point data into cell data.

        Point data are specified per node and cell data specified within cells.
        Optionally, the input point data can be passed through to the output.

        See also: :func:`pyvista.DataSetFilters.cell_data_to_point_data`

        Parameters
        ----------
        pass_point_data : bool, default: False
            If enabled, pass the input point data through to the output.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with the point data transformed into cell data.
            Return type matches input.

        Examples
        --------
        Color cells by their z coordinates.  First, create point
        scalars based on z-coordinates of a sample sphere mesh.  Then
        convert this point data to cell data.  Use a low resolution
        sphere for emphasis of cell valued data.

        First, plot these values as point values to show the
        difference between point and cell data.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(theta_resolution=10, phi_resolution=10)
        >>> sphere['Z Coordinates'] = sphere.points[:, 2]
        >>> sphere.plot()

        Now, convert these values to cell data and then plot it.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(theta_resolution=10, phi_resolution=10)
        >>> sphere['Z Coordinates'] = sphere.points[:, 2]
        >>> sphere = sphere.point_data_to_cell_data()
        >>> sphere.plot()

        """
        alg = _vtk.vtkPointDataToCellData()
        alg.SetInputDataObject(self)
        alg.SetPassPointData(pass_point_data)
        _update_alg(alg, progress_bar, 'Transforming point data into cell data')
        active_scalars = None
        if not isinstance(self, pyvista.MultiBlock):
            active_scalars = self.active_scalars_name
        return _get_output(alg, active_scalars=active_scalars)

    def ptc(self, pass_point_data=False, progress_bar=False, **kwargs):
        """Transform point data into cell data.

        Point data are specified per node and cell data specified
        within cells.  Optionally, the input point data can be passed
        through to the output.

        This method is an alias for
        :func:`pyvista.DataSetFilters.point_data_to_cell_data`.

        Parameters
        ----------
        pass_point_data : bool, default: False
            If enabled, pass the input point data through to the output.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            Depreciated keyword argument ``pass_point_arrays``.

        Returns
        -------
        pyvista.DataSet
            Dataset with the point data transformed into cell data.
            Return type matches input.

        """
        return DataSetFilters.point_data_to_cell_data(
            self, pass_point_data=pass_point_data, progress_bar=progress_bar, **kwargs
        )

    def triangulate(self, inplace=False, progress_bar=False):
        """Return an all triangle mesh.

        More complex polygons will be broken down into triangles.

        Parameters
        ----------
        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing only triangles.

        Examples
        --------
        Generate a mesh with quadrilateral faces.

        >>> import pyvista
        >>> plane = pyvista.Plane()
        >>> plane.point_data.clear()
        >>> plane.plot(show_edges=True, line_width=5)

        Convert it to an all triangle mesh.

        >>> mesh = plane.triangulate()
        >>> mesh.plot(show_edges=True, line_width=5)

        """
        alg = _vtk.vtkDataSetTriangleFilter()
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Converting to triangle mesh')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def delaunay_3d(self, alpha=0.0, tol=0.001, offset=2.5, progress_bar=False):
        """Construct a 3D Delaunay triangulation of the mesh.

        This filter can be used to generate a 3D tetrahedral mesh from
        a surface or scattered points.  If you want to create a
        surface from a point cloud, see
        :func:`pyvista.PolyDataFilters.reconstruct_surface`.

        Parameters
        ----------
        alpha : float, default: 0.0
            Distance value to control output of this filter. For a
            non-zero alpha value, only vertices, edges, faces, or
            tetrahedra contained within the circumsphere (of radius
            alpha) will be output. Otherwise, only tetrahedra will be
            output.

        tol : float, default: 0.001
            Tolerance to control discarding of closely spaced points.
            This tolerance is specified as a fraction of the diagonal
            length of the bounding box of the points.

        offset : float, default: 2.5
            Multiplier to control the size of the initial, bounding
            Delaunay triangulation.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid containing the Delaunay triangulation.

        Examples
        --------
        Generate a 3D Delaunay triangulation of a surface mesh of a
        sphere and plot the interior edges generated.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(theta_resolution=5, phi_resolution=5)
        >>> grid = sphere.delaunay_3d()
        >>> edges = grid.extract_all_edges()
        >>> edges.plot(line_width=5, color='k')

        """
        alg = _vtk.vtkDelaunay3D()
        alg.SetInputData(self)
        alg.SetAlpha(alpha)
        alg.SetTolerance(tol)
        alg.SetOffset(offset)
        _update_alg(alg, progress_bar, 'Computing 3D Triangulation')
        return _get_output(alg)

    def select_enclosed_points(
        self, surface, tolerance=0.001, inside_out=False, check_surface=True, progress_bar=False
    ):
        """Mark points as to whether they are inside a closed surface.

        This evaluates all the input points to determine whether they are in an
        enclosed surface. The filter produces a (0,1) mask
        (in the form of a vtkDataArray) that indicates whether points are
        outside (mask value=0) or inside (mask value=1) a provided surface.
        (The name of the output vtkDataArray is ``"SelectedPoints"``.)

        This filter produces and output data array, but does not modify the
        input dataset. If you wish to extract cells or poinrs, various
        threshold filters are available (i.e., threshold the output array).

        .. warning::
           The filter assumes that the surface is closed and
           manifold. A boolean flag can be set to force the filter to
           first check whether this is true. If ``False`` and not manifold,
           an error will be raised.

        Parameters
        ----------
        surface : pyvista.PolyData
            Set the surface to be used to test for containment. This must be a
            :class:`pyvista.PolyData` object.

        tolerance : float, default: 0.001
            The tolerance on the intersection. The tolerance is expressed as a
            fraction of the bounding box of the enclosing surface.

        inside_out : bool, default: False
            By default, points inside the surface are marked inside or sent
            to the output. If ``inside_out`` is ``True``, then the points
            outside the surface are marked inside.

        check_surface : bool, default: True
            Specify whether to check the surface for closure. When ``True``, the
            algorithm first checks to see if the surface is closed and
            manifold. If the surface is not closed and manifold, a runtime
            error is raised.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing the ``point_data['SelectedPoints']`` array.

        Examples
        --------
        Determine which points on a plane are inside a manifold sphere
        surface mesh.  Extract these points using the
        :func:`DataSetFilters.extract_points` filter and then plot them.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> plane = pyvista.Plane()
        >>> selected = plane.select_enclosed_points(sphere)
        >>> pts = plane.extract_points(
        ...     selected['SelectedPoints'].view(bool),
        ...     adjacent_cells=False,
        ... )
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(sphere, style='wireframe')
        >>> _ = pl.add_points(pts, color='r')
        >>> pl.show()

        """
        if not isinstance(surface, pyvista.PolyData):
            raise TypeError("`surface` must be `pyvista.PolyData`")
        if check_surface and surface.n_open_edges > 0:
            raise RuntimeError(
                "Surface is not closed. Please read the warning in the "
                "documentation for this function and either pass "
                "`check_surface=False` or repair the surface."
            )
        alg = _vtk.vtkSelectEnclosedPoints()
        alg.SetInputData(self)
        alg.SetSurfaceData(surface)
        alg.SetTolerance(tolerance)
        alg.SetInsideOut(inside_out)
        _update_alg(alg, progress_bar, 'Selecting Enclosed Points')
        result = _get_output(alg)
        out = self.copy()
        bools = result['SelectedPoints'].astype(np.uint8)
        if len(bools) < 1:
            bools = np.zeros(out.n_points, dtype=np.uint8)
        out['SelectedPoints'] = bools
        return out

    def probe(
        self,
        points,
        tolerance=None,
        pass_cell_data=True,
        pass_point_data=True,
        categorical=False,
        progress_bar=False,
        locator=None,
    ):
        """Sample data values at specified point locations.

        This uses :class:`vtk.vtkProbeFilter`.

        Parameters
        ----------
        points : pyvista.DataSet
            The points to probe values on to. This should be a PyVista mesh
            or something :func:`pyvista.wrap` can handle.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        pass_cell_data : bool, default: True
            Preserve source mesh's original cell data arrays.

        pass_point_data : bool, default: True
            Preserve source mesh's original point data arrays.

        categorical : bool, default: False
            Control whether the source point data is to be treated as
            categorical. If the data is categorical, then the resultant data
            will be determined by a nearest neighbor interpolation scheme.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        locator : vtkAbstractCellLocator, optional
            Prototype cell locator to perform the ``FindCell()``
            operation.

        Returns
        -------
        pyvista.DataSet
            Dataset containing the probed data.

        Examples
        --------
        Probe the active scalars in ``grid`` at the points in ``mesh``.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> mesh = pv.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
        >>> grid = examples.load_uniform()
        >>> result = grid.probe(mesh)
        >>> 'Spatial Point Data' in result.point_data
        True

        """
        if not pyvista.is_pyvista_dataset(points):
            points = pyvista.wrap(points)
        alg = _vtk.vtkProbeFilter()
        alg.SetInputData(points)
        alg.SetSourceData(self)
        alg.SetPassCellArrays(pass_cell_data)
        alg.SetPassPointArrays(pass_point_data)
        alg.SetCategoricalData(categorical)

        if tolerance is not None:
            alg.SetComputeTolerance(False)
            alg.SetTolerance(tolerance)

        if locator:
            alg.SetCellLocatorPrototype(locator)

        _update_alg(alg, progress_bar, 'Sampling Data Values at Specified Point Locations')
        return _get_output(alg)

    def sample(
        self,
        target,
        tolerance=None,
        pass_cell_data=True,
        pass_point_data=True,
        categorical=False,
        progress_bar=False,
    ):
        """Resample array data from a passed mesh onto this mesh.

        This uses :class:`vtk.vtkResampleWithDataSet`.

        Parameters
        ----------
        target : pyvista.DataSet
            The vtk data object to sample from - point and cell arrays from
            this object are sampled onto the nodes of the ``dataset`` mesh.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        pass_cell_data : bool, default: True
            Preserve source mesh's original cell data arrays.

        pass_point_data : bool, default: True
            Preserve source mesh's original point data arrays.

        categorical : bool, default: False
            Control whether the source point data is to be treated as
            categorical. If the data is categorical, then the resultant data
            will be determined by a nearest neighbor interpolation scheme.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset containing resampled data.

        Examples
        --------
        Resample data from another dataset onto a sphere.

        >>> import pyvista
        >>> from pyvista import examples
        >>> mesh = pyvista.Sphere(center=(4.5, 4.5, 4.5), radius=4.5)
        >>> data_to_probe = examples.load_uniform()
        >>> result = mesh.sample(data_to_probe)
        >>> result.plot(scalars="Spatial Point Data")

        See :ref:`resampling_example` for more examples using this filter.

        """
        if not pyvista.is_pyvista_dataset(target):
            raise TypeError('`target` must be a PyVista mesh type.')
        alg = _vtk.vtkResampleWithDataSet()  # Construct the ResampleWithDataSet object
        alg.SetInputData(self)  # Set the Input data (actually the source i.e. where to sample from)
        # Set the Source data (actually the target, i.e. where to sample to)
        alg.SetSourceData(target)
        alg.SetPassCellArrays(pass_cell_data)
        alg.SetPassPointArrays(pass_point_data)
        alg.SetCategoricalData(categorical)
        if tolerance is not None:
            alg.SetComputeTolerance(False)
            alg.SetTolerance(tolerance)
        _update_alg(alg, progress_bar, 'Resampling array Data from a Passed Mesh onto Mesh')
        return _get_output(alg)

    def interpolate(
        self,
        target,
        sharpness=2.0,
        radius=1.0,
        strategy='null_value',
        null_value=0.0,
        n_points=None,
        pass_cell_data=True,
        pass_point_data=True,
        progress_bar=False,
    ):
        """Interpolate values onto this mesh from a given dataset.

        The input dataset is typically a point cloud. Only point data from
        the source mesh will be interpolated onto points of this mesh. Whether
        preexisting point and cell data of this mesh are preserved in the
        output can be customized with the ``pass_point_data`` and
        ``pass_cell_data`` parameters.

        This uses a Gaussian interpolation kernel. Use the ``sharpness`` and
        ``radius`` parameters to adjust this kernel. You can also switch this
        kernel to use an N closest points approach.

        Parameters
        ----------
        target : pyvista.DataSet
            The vtk data object to sample from. Point and cell arrays from
            this object are interpolated onto this mesh.

        sharpness : float, default: 2.0
            Set the sharpness (i.e., falloff) of the Gaussian kernel. As the
            sharpness increases the effects of distant points are reduced.

        radius : float, optional
            Specify the radius within which the basis points must lie.

        strategy : str, default: "null_value"
            Specify a strategy to use when encountering a "null" point during
            the interpolation process. Null points occur when the local
            neighborhood (of nearby points to interpolate from) is empty. If
            the strategy is set to ``'mask_points'``, then an output array is
            created that marks points as being valid (=1) or null (invalid =0)
            (and the NullValue is set as well). If the strategy is set to
            ``'null_value'``, then the output data value(s) are set to the
            ``null_value`` (specified in the output point data). Finally, the
            strategy ``'closest_point'`` is to simply use the closest point to
            perform the interpolation.

        null_value : float, default: 0.0
            Specify the null point value. When a null point is encountered
            then all components of each null tuple are set to this value.

        n_points : int, optional
            If given, specifies the number of the closest points used to form
            the interpolation basis. This will invalidate the radius argument
            in favor of an N closest points approach. This typically has poorer
            results.

        pass_cell_data : bool, default: True
            Preserve input mesh's original cell data arrays.

        pass_point_data : bool, default: True
            Preserve input mesh's original point data arrays.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Interpolated dataset.  Return type matches input.

        Examples
        --------
        Interpolate the values of 5 points onto a sample plane.

        >>> import pyvista
        >>> import numpy as np
        >>> np.random.seed(7)
        >>> point_cloud = np.random.random((5, 3))
        >>> point_cloud[:, 2] = 0
        >>> point_cloud -= point_cloud.mean(0)
        >>> pdata = pyvista.PolyData(point_cloud)
        >>> pdata['values'] = np.random.random(5)
        >>> plane = pyvista.Plane()
        >>> plane.clear_data()
        >>> plane = plane.interpolate(pdata, sharpness=3)
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     pdata, render_points_as_spheres=True, point_size=50
        ... )
        >>> _ = pl.add_mesh(plane, style='wireframe', line_width=5)
        >>> pl.show()

        See :ref:`interpolate_example` for more examples using this filter.

        """
        if not pyvista.is_pyvista_dataset(target):
            raise TypeError('`target` must be a PyVista mesh type.')

        # Must cast to UnstructuredGrid in some cases (e.g. vtkImageData/vtkRectilinearGrid)
        # I believe the locator and the interpolator call `GetPoints` and not all mesh types have that method
        if isinstance(target, (pyvista.UniformGrid, pyvista.RectilinearGrid)):
            target = target.cast_to_unstructured_grid()

        gaussian_kernel = _vtk.vtkGaussianKernel()
        gaussian_kernel.SetSharpness(sharpness)
        gaussian_kernel.SetRadius(radius)
        gaussian_kernel.SetKernelFootprintToRadius()
        if n_points:
            gaussian_kernel.SetNumberOfPoints(n_points)
            gaussian_kernel.SetKernelFootprintToNClosest()

        locator = _vtk.vtkStaticPointLocator()
        locator.SetDataSet(target)
        locator.BuildLocator()

        interpolator = _vtk.vtkPointInterpolator()
        interpolator.SetInputData(self)
        interpolator.SetSourceData(target)
        interpolator.SetKernel(gaussian_kernel)
        interpolator.SetLocator(locator)
        interpolator.SetNullValue(null_value)
        if strategy == 'null_value':
            interpolator.SetNullPointsStrategyToNullValue()
        elif strategy == 'mask_points':
            interpolator.SetNullPointsStrategyToMaskPoints()
        elif strategy == 'closest_point':
            interpolator.SetNullPointsStrategyToClosestPoint()
        else:
            raise ValueError(f'strategy `{strategy}` not supported.')
        interpolator.SetPassPointArrays(pass_point_data)
        interpolator.SetPassCellArrays(pass_cell_data)
        _update_alg(interpolator, progress_bar, 'Interpolating')
        return _get_output(interpolator)

    def streamlines(
        self,
        vectors=None,
        source_center=None,
        source_radius=None,
        n_points=100,
        start_position=None,
        return_source=False,
        pointa=None,
        pointb=None,
        progress_bar=False,
        **kwargs,
    ):
        """Integrate a vector field to generate streamlines.

        The default behavior uses a sphere as the source - set its
        location and radius via the ``source_center`` and
        ``source_radius`` keyword arguments.  ``n_points`` defines the
        number of starting points on the sphere surface.
        Alternatively, a line source can be used by specifying
        ``pointa`` and ``pointb``.  ``n_points`` again defines the
        number of points on the line.

        You can retrieve the source by specifying
        ``return_source=True``.

        Optional keyword parameters from
        :func:`pyvista.DataSetFilters.streamlines_from_source` can be
        used here to control the generation of streamlines.

        Parameters
        ----------
        vectors : str, optional
            The string name of the active vector field to integrate across.

        source_center : sequence[float], optional
            Length 3 tuple of floats defining the center of the source
            particles. Defaults to the center of the dataset.

        source_radius : float, optional
            Float radius of the source particle cloud. Defaults to one-tenth of
            the diagonal of the dataset's spatial extent.

        n_points : int, default: 100
            Number of particles present in source sphere or line.

        start_position : sequence[float], optional
            A single point.  This will override the sphere point source.

        return_source : bool, default: False
            Return the source particles as :class:`pyvista.PolyData` as well as the
            streamlines. This will be the second value returned if ``True``.

        pointa, pointb : sequence[float], optional
            The coordinates of a start and end point for a line source. This
            will override the sphere and start_position point source.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            See :func:`pyvista.DataSetFilters.streamlines_from_source`.

        Returns
        -------
        streamlines : pyvista.PolyData
            This produces polylines as the output, with each cell
            (i.e., polyline) representing a streamline. The attribute values
            associated with each streamline are stored in the cell data, whereas
            those associated with streamline-points are stored in the point data.

        source : pyvista.PolyData
            The points of the source are the seed points for the streamlines.
            Only returned if ``return_source=True``.

        Examples
        --------
        See the :ref:`streamlines_example` example.

        """
        if source_center is None:
            source_center = self.center
        if source_radius is None:
            source_radius = self.length / 10.0

        # A single point at start_position
        if start_position is not None:
            source_center = start_position
            source_radius = 0.0
            n_points = 1

        if (pointa is not None and pointb is None) or (pointa is None and pointb is not None):
            raise ValueError("Both pointa and pointb must be provided")
        elif pointa is not None and pointb is not None:
            source = _vtk.vtkLineSource()
            source.SetPoint1(pointa)
            source.SetPoint2(pointb)
            source.SetResolution(n_points)
        else:
            source = _vtk.vtkPointSource()
            source.SetCenter(source_center)
            source.SetRadius(source_radius)
            source.SetNumberOfPoints(n_points)
        source.Update()
        input_source = pyvista.wrap(source.GetOutput())
        output = self.streamlines_from_source(
            input_source, vectors, progress_bar=progress_bar, **kwargs
        )
        if return_source:
            return output, input_source
        return output

    def streamlines_from_source(
        self,
        source,
        vectors=None,
        integrator_type=45,
        integration_direction='both',
        surface_streamlines=False,
        initial_step_length=0.5,
        step_unit='cl',
        min_step_length=0.01,
        max_step_length=1.0,
        max_steps=2000,
        terminal_speed=1e-12,
        max_error=1e-6,
        max_time=None,
        compute_vorticity=True,
        rotation_scale=1.0,
        interpolator_type='point',
        progress_bar=False,
    ):
        """Generate streamlines of vectors from the points of a source mesh.

        The integration is performed using a specified integrator, by default
        Runge-Kutta2. This supports integration through any type of dataset.
        If the dataset contains 2D cells like polygons or triangles and the
        ``surface_streamlines`` parameter is used, the integration is constrained
        to lie on the surface defined by 2D cells.

        Parameters
        ----------
        source : pyvista.DataSet
            The points of the source provide the starting points of the
            streamlines.  This will override both sphere and line sources.

        vectors : str, optional
            The string name of the active vector field to integrate across.

        integrator_type : {45, 2, 4}, default: 45
            The integrator type to be used for streamline generation.
            The default is Runge-Kutta45. The recognized solvers are:
            RUNGE_KUTTA2 (``2``),  RUNGE_KUTTA4 (``4``), and RUNGE_KUTTA45
            (``45``). Options are ``2``, ``4``, or ``45``.

        integration_direction : str, default: "both"
            Specify whether the streamline is integrated in the upstream or
            downstream directions (or both). Options are ``'both'``,
            ``'backward'``, or ``'forward'``.

        surface_streamlines : bool, default: False
            Compute streamlines on a surface.

        initial_step_length : float, default: 0.5
            Initial step size used for line integration, expressed ib length
            unitsL or cell length units (see ``step_unit`` parameter).
            either the starting size for an adaptive integrator, e.g., RK45, or
            the constant / fixed size for non-adaptive ones, i.e., RK2 and RK4).

        step_unit : {'cl', 'l'}, default: "cl"
            Uniform integration step unit. The valid unit is now limited to
            only LENGTH_UNIT (``'l'``) and CELL_LENGTH_UNIT (``'cl'``).
            Default is CELL_LENGTH_UNIT.

        min_step_length : float, default: 0.01
            Minimum step size used for line integration, expressed in length or
            cell length units. Only valid for an adaptive integrator, e.g., RK45.

        max_step_length : float, default: 1.0
            Maximum step size used for line integration, expressed in length or
            cell length units. Only valid for an adaptive integrator, e.g., RK45.

        max_steps : int, default: 2000
            Maximum number of steps for integrating a streamline.

        terminal_speed : float, default: 1e-12
            Terminal speed value, below which integration is terminated.

        max_error : float, 1e-6
            Maximum error tolerated throughout streamline integration.

        max_time : float, optional
            Specify the maximum length of a streamline expressed in LENGTH_UNIT.

        compute_vorticity : bool, default: True
            Vorticity computation at streamline points. Necessary for generating
            proper stream-ribbons using the ``vtkRibbonFilter``.

        rotation_scale : float, default: 1.0
            This can be used to scale the rate with which the streamribbons
            twist.

        interpolator_type : str, default: "point"
            Set the type of the velocity field interpolator to locate cells
            during streamline integration either by points or cells.
            The cell locator is more robust then the point locator. Options
            are ``'point'`` or ``'cell'`` (abbreviations of ``'p'`` and ``'c'``
            are also supported).

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Streamlines. This produces polylines as the output, with
            each cell (i.e., polyline) representing a streamline. The
            attribute values associated with each streamline are
            stored in the cell data, whereas those associated with
            streamline-points are stored in the point data.

        Examples
        --------
        See the :ref:`streamlines_example` example.

        """
        integration_direction = str(integration_direction).strip().lower()
        if integration_direction not in ['both', 'back', 'backward', 'forward']:
            raise ValueError(
                "Integration direction must be one of:\n 'backward', "
                f"'forward', or 'both' - not '{integration_direction}'."
            )
        if integrator_type not in [2, 4, 45]:
            raise ValueError('Integrator type must be one of `2`, `4`, or `45`.')
        if interpolator_type not in ['c', 'cell', 'p', 'point']:
            raise ValueError("Interpolator type must be either 'cell' or 'point'")
        if step_unit not in ['l', 'cl']:
            raise ValueError("Step unit must be either 'l' or 'cl'")
        step_unit = {
            'cl': _vtk.vtkStreamTracer.CELL_LENGTH_UNIT,
            'l': _vtk.vtkStreamTracer.LENGTH_UNIT,
        }[step_unit]
        if isinstance(vectors, str):
            self.set_active_scalars(vectors)
            self.set_active_vectors(vectors)
        elif vectors is None:
            pyvista.set_default_active_vectors(self)

        if max_time is None:
            max_velocity = self.get_data_range()[-1]
            max_time = 4.0 * self.GetLength() / max_velocity
        if not isinstance(source, pyvista.DataSet):
            raise TypeError("source must be a pyvista.DataSet")

        # vtk throws error with two Structured Grids
        # See: https://github.com/pyvista/pyvista/issues/1373
        if isinstance(self, pyvista.StructuredGrid) and isinstance(source, pyvista.StructuredGrid):
            source = source.cast_to_unstructured_grid()

        # Build the algorithm
        alg = _vtk.vtkStreamTracer()
        # Inputs
        alg.SetInputDataObject(self)
        alg.SetSourceData(source)

        # general parameters
        alg.SetComputeVorticity(compute_vorticity)
        alg.SetInitialIntegrationStep(initial_step_length)
        alg.SetIntegrationStepUnit(step_unit)
        alg.SetMaximumError(max_error)
        alg.SetMaximumIntegrationStep(max_step_length)
        alg.SetMaximumNumberOfSteps(max_steps)
        alg.SetMaximumPropagation(max_time)
        alg.SetMinimumIntegrationStep(min_step_length)
        alg.SetRotationScale(rotation_scale)
        alg.SetSurfaceStreamlines(surface_streamlines)
        alg.SetTerminalSpeed(terminal_speed)
        # Model parameters
        if integration_direction == 'forward':
            alg.SetIntegrationDirectionToForward()
        elif integration_direction in ['backward', 'back']:
            alg.SetIntegrationDirectionToBackward()
        else:
            alg.SetIntegrationDirectionToBoth()
        # set integrator type
        if integrator_type == 2:
            alg.SetIntegratorTypeToRungeKutta2()
        elif integrator_type == 4:
            alg.SetIntegratorTypeToRungeKutta4()
        else:
            alg.SetIntegratorTypeToRungeKutta45()
        # set interpolator type
        if interpolator_type in ['c', 'cell']:
            alg.SetInterpolatorTypeToCellLocator()
        else:
            alg.SetInterpolatorTypeToDataSetPointLocator()
        # run the algorithm
        _update_alg(alg, progress_bar, 'Generating Streamlines')
        return _get_output(alg)

    def streamlines_evenly_spaced_2D(
        self,
        vectors=None,
        start_position=None,
        integrator_type=2,
        step_length=0.5,
        step_unit='cl',
        max_steps=2000,
        terminal_speed=1e-12,
        interpolator_type='point',
        separating_distance=10,
        separating_distance_ratio=0.5,
        closed_loop_maximum_distance=0.5,
        loop_angle=20,
        minimum_number_of_loop_points=4,
        compute_vorticity=True,
        progress_bar=False,
    ):
        """Generate evenly spaced streamlines on a 2D dataset.

        This filter only supports datasets that lie on the xy plane, i.e. ``z=0``.
        Particular care must be used to choose a `separating_distance`
        that do not result in too much memory being utilized.  The
        default unit is cell length.

        Parameters
        ----------
        vectors : str, optional
            The string name of the active vector field to integrate across.

        start_position : sequence[float], optional
            The seed point for generating evenly spaced streamlines.
            If not supplied, a random position in the dataset is chosen.

        integrator_type : {2, 4}, default: 2
            The integrator type to be used for streamline generation.
            The default is Runge-Kutta2. The recognized solvers are:
            RUNGE_KUTTA2 (``2``) and RUNGE_KUTTA4 (``4``).

        step_length : float, default: 0.5
            Constant Step size used for line integration, expressed in length
            units or cell length units (see ``step_unit`` parameter).

        step_unit : {'cl', 'l'}, default: "cl"
            Uniform integration step unit. The valid unit is now limited to
            only LENGTH_UNIT (``'l'``) and CELL_LENGTH_UNIT (``'cl'``).
            Default is CELL_LENGTH_UNIT.

        max_steps : int, default: 2000
            Maximum number of steps for integrating a streamline.

        terminal_speed : float, default: 1e-12
            Terminal speed value, below which integration is terminated.

        interpolator_type : str, optional
            Set the type of the velocity field interpolator to locate cells
            during streamline integration either by points or cells.
            The cell locator is more robust then the point locator. Options
            are ``'point'`` or ``'cell'`` (abbreviations of ``'p'`` and ``'c'``
            are also supported).

        separating_distance : float, default: 10
            The distance between streamlines expressed in ``step_unit``.

        separating_distance_ratio : float, default: 0.5
            Streamline integration is stopped if streamlines are closer than
            ``SeparatingDistance*SeparatingDistanceRatio`` to other streamlines.

        closed_loop_maximum_distance : float, default: 0.5
            The distance between points on a streamline to determine a
            closed loop.

        loop_angle : float, default: 20
            The maximum angle in degrees between points to determine a closed loop.

        minimum_number_of_loop_points : int, default: 4
            The minimum number of points before which a closed loop will
            be determined.

        compute_vorticity : bool, default: True
            Vorticity computation at streamline points. Necessary for generating
            proper stream-ribbons using the ``vtkRibbonFilter``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            This produces polylines as the output, with each cell
            (i.e., polyline) representing a streamline. The attribute
            values associated with each streamline are stored in the
            cell data, whereas those associated with streamline-points
            are stored in the point data.

        Examples
        --------
        Plot evenly spaced streamlines for cylinder in a crossflow.
        This dataset is a multiblock dataset, and the fluid velocity is in the
        first block.

        >>> import pyvista
        >>> from pyvista import examples
        >>> mesh = examples.download_cylinder_crossflow()
        >>> streams = mesh[0].streamlines_evenly_spaced_2D(
        ...     start_position=(4, 0.1, 0.0),
        ...     separating_distance=3,
        ...     separating_distance_ratio=0.2,
        ... )
        >>> plotter = pyvista.Plotter()
        >>> _ = plotter.add_mesh(
        ...     streams.tube(radius=0.02), scalars="vorticity_mag"
        ... )
        >>> plotter.view_xy()
        >>> plotter.show()

        See :ref:`2d_streamlines_example` for more examples using this filter.
        """
        if integrator_type not in [2, 4]:
            raise ValueError('Integrator type must be one of `2` or `4`.')
        if interpolator_type not in ['c', 'cell', 'p', 'point']:
            raise ValueError("Interpolator type must be either 'cell' or 'point'")
        if step_unit not in ['l', 'cl']:
            raise ValueError("Step unit must be either 'l' or 'cl'")
        step_unit = {
            'cl': _vtk.vtkStreamTracer.CELL_LENGTH_UNIT,
            'l': _vtk.vtkStreamTracer.LENGTH_UNIT,
        }[step_unit]
        if isinstance(vectors, str):
            self.set_active_scalars(vectors)
            self.set_active_vectors(vectors)
        elif vectors is None:
            pyvista.set_default_active_vectors(self)

        loop_angle = loop_angle * np.pi / 180

        # Build the algorithm
        alg = _vtk.vtkEvenlySpacedStreamlines2D()
        # Inputs
        alg.SetInputDataObject(self)

        # Seed for starting position
        if start_position is not None:
            alg.SetStartPosition(start_position)

        # Integrator controls
        if integrator_type == 2:
            alg.SetIntegratorTypeToRungeKutta2()
        else:
            alg.SetIntegratorTypeToRungeKutta4()
        alg.SetInitialIntegrationStep(step_length)
        alg.SetIntegrationStepUnit(step_unit)
        alg.SetMaximumNumberOfSteps(max_steps)

        # Stopping criteria
        alg.SetTerminalSpeed(terminal_speed)
        alg.SetClosedLoopMaximumDistance(closed_loop_maximum_distance)
        alg.SetLoopAngle(loop_angle)
        alg.SetMinimumNumberOfLoopPoints(minimum_number_of_loop_points)

        # Separation criteria
        alg.SetSeparatingDistance(separating_distance)
        if separating_distance_ratio is not None:
            alg.SetSeparatingDistanceRatio(separating_distance_ratio)

        alg.SetComputeVorticity(compute_vorticity)

        # Set interpolator type
        if interpolator_type in ['c', 'cell']:
            alg.SetInterpolatorTypeToCellLocator()
        else:
            alg.SetInterpolatorTypeToDataSetPointLocator()

        # Run the algorithm
        _update_alg(alg, progress_bar, 'Generating Evenly Spaced Streamlines on a 2D Dataset')
        return _get_output(alg)

    def decimate_boundary(self, target_reduction=0.5, progress_bar=False):
        """Return a decimated version of a triangulation of the boundary.

        Only the outer surface of the input dataset will be considered.

        Parameters
        ----------
        target_reduction : float, default: 0.5
            Fraction of the original mesh to remove.
            TargetReduction is set to ``0.9``, this filter will try to reduce
            the data set to 10% of its original size and will remove 90%
            of the input triangles.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Decimated boundary.

        Examples
        --------
        See the :ref:`linked_views_example` example.

        """
        return (
            self.extract_geometry(progress_bar=progress_bar)
            .triangulate()
            .decimate(target_reduction)
        )

    def sample_over_line(self, pointa, pointb, resolution=None, tolerance=None, progress_bar=False):
        """Sample a dataset onto a line.

        Parameters
        ----------
        pointa : sequence[float]
            Location in ``[x, y, z]``.

        pointb : sequence[float]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide line into. Defaults to number of cells
            in the input mesh. Must be a positive integer.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is in a
            cell of the input.  If not given, tolerance is automatically generated.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Line object with sampled data from dataset.

        Examples
        --------
        Sample over a plane that is interpolating a point cloud.

        >>> import pyvista
        >>> import numpy as np
        >>> np.random.seed(12)
        >>> point_cloud = np.random.random((5, 3))
        >>> point_cloud[:, 2] = 0
        >>> point_cloud -= point_cloud.mean(0)
        >>> pdata = pyvista.PolyData(point_cloud)
        >>> pdata['values'] = np.random.random(5)
        >>> plane = pyvista.Plane()
        >>> plane.clear_data()
        >>> plane = plane.interpolate(pdata, sharpness=3.5)
        >>> sample = plane.sample_over_line((-0.5, -0.5, 0), (0.5, 0.5, 0))
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     pdata, render_points_as_spheres=True, point_size=50
        ... )
        >>> _ = pl.add_mesh(sample, scalars='values', line_width=10)
        >>> _ = pl.add_mesh(plane, scalars='values', style='wireframe')
        >>> pl.show()

        """
        if resolution is None:
            resolution = int(self.n_cells)
        # Make a line and sample the dataset
        line = pyvista.Line(pointa, pointb, resolution=resolution)
        sampled_line = line.sample(self, tolerance=tolerance, progress_bar=progress_bar)
        return sampled_line

    def plot_over_line(
        self,
        pointa,
        pointb,
        resolution=None,
        scalars=None,
        title=None,
        ylabel=None,
        figsize=None,
        figure=True,
        show=True,
        tolerance=None,
        fname=None,
        progress_bar=False,
    ):
        """Sample a dataset along a high resolution line and plot.

        Plot the variables of interest in 2D using matplotlib where the
        X-axis is distance from Point A and the Y-axis is the variable
        of interest. Note that this filter returns ``None``.

        Parameters
        ----------
        pointa : sequence[float]
            Location in ``[x, y, z]``.

        pointb : sequence[float]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide line into. Defaults to number of cells
            in the input mesh. Must be a positive integer.

        scalars : str, optional
            The string name of the variable in the input dataset to probe. The
            active scalar is used by default.

        title : str, optional
            The string title of the matplotlib figure.

        ylabel : str, optional
            The string label of the Y-axis. Defaults to variable name.

        figsize : tuple(int), optional
            The size of the new figure.

        figure : bool, default: True
            Flag on whether or not to create a new figure.

        show : bool, default: True
            Shows the matplotlib figure.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is in a
            cell of the input.  If not given, tolerance is automatically generated.

        fname : str, optional
            Save the figure this file name when set.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Examples
        --------
        See the :ref:`plot_over_line_example` example.

        """
        # Sample on line
        sampled = DataSetFilters.sample_over_line(
            self, pointa, pointb, resolution, tolerance, progress_bar=progress_bar
        )

        # Get variable of interest
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        values = sampled.get_array(scalars)
        distance = sampled['Distance']

        # Remainder is plotting
        if figure:
            plt.figure(figsize=figsize)
        # Plot it in 2D
        if values.ndim > 1:
            for i in range(values.shape[1]):
                plt.plot(distance, values[:, i], label=f'Component {i}')
            plt.legend()
        else:
            plt.plot(distance, values)
        plt.xlabel('Distance')
        if ylabel is None:
            plt.ylabel(scalars)
        else:
            plt.ylabel(ylabel)
        if title is None:
            plt.title(f'{scalars} Profile')
        else:
            plt.title(title)
        if fname:
            plt.savefig(fname)
        if show:  # pragma: no cover
            plt.show()

    def sample_over_multiple_lines(self, points, tolerance=None, progress_bar=False):
        """Sample a dataset onto a multiple lines.

        Parameters
        ----------
        points : array_like[float]
            List of points defining multiple lines.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is in a
            cell of the input.  If not given, tolerance is automatically generated.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Line object with sampled data from dataset.

        Examples
        --------
        Sample over a plane that is interpolating a point cloud.

        >>> import pyvista
        >>> import numpy as np
        >>> np.random.seed(12)
        >>> point_cloud = np.random.random((5, 3))
        >>> point_cloud[:, 2] = 0
        >>> point_cloud -= point_cloud.mean(0)
        >>> pdata = pyvista.PolyData(point_cloud)
        >>> pdata['values'] = np.random.random(5)
        >>> plane = pyvista.Plane()
        >>> plane.clear_data()
        >>> plane = plane.interpolate(pdata, sharpness=3.5)
        >>> sample = plane.sample_over_multiple_lines(
        ...     [[-0.5, -0.5, 0], [0.5, -0.5, 0], [0.5, 0.5, 0]]
        ... )
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     pdata, render_points_as_spheres=True, point_size=50
        ... )
        >>> _ = pl.add_mesh(sample, scalars='values', line_width=10)
        >>> _ = pl.add_mesh(plane, scalars='values', style='wireframe')
        >>> pl.show()

        """
        # Make a multiple lines and sample the dataset
        multiple_lines = pyvista.MultipleLines(points=points)
        sampled_multiple_lines = multiple_lines.sample(
            self, tolerance=tolerance, progress_bar=progress_bar
        )
        return sampled_multiple_lines

    def sample_over_circular_arc(
        self, pointa, pointb, center, resolution=None, tolerance=None, progress_bar=False
    ):
        """Sample a dataset over a circular arc.

        Parameters
        ----------
        pointa : sequence[float]
            Location in ``[x, y, z]``.

        pointb : sequence[float]
            Location in ``[x, y, z]``.

        center : sequence[float]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide circular arc into. Defaults to
            number of cells in the input mesh. Must be a positive
            integer.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Arc containing the sampled data.

        Examples
        --------
        Sample a dataset over a circular arc and plot it.

        >>> import pyvista
        >>> from pyvista import examples
        >>> uniform = examples.load_uniform()
        >>> uniform["height"] = uniform.points[:, 2]
        >>> pointa = [
        ...     uniform.bounds[1],
        ...     uniform.bounds[2],
        ...     uniform.bounds[5],
        ... ]
        >>> pointb = [
        ...     uniform.bounds[1],
        ...     uniform.bounds[3],
        ...     uniform.bounds[4],
        ... ]
        >>> center = [
        ...     uniform.bounds[1],
        ...     uniform.bounds[2],
        ...     uniform.bounds[4],
        ... ]
        >>> sampled_arc = uniform.sample_over_circular_arc(
        ...     pointa, pointb, center
        ... )
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(uniform, style='wireframe')
        >>> _ = pl.add_mesh(sampled_arc, line_width=10)
        >>> pl.show_axes()
        >>> pl.show()

        """
        if resolution is None:
            resolution = int(self.n_cells)
        # Make a circular arc and sample the dataset
        circular_arc = pyvista.CircularArc(pointa, pointb, center, resolution=resolution)
        sampled_circular_arc = circular_arc.sample(
            self, tolerance=tolerance, progress_bar=progress_bar
        )
        return sampled_circular_arc

    def sample_over_circular_arc_normal(
        self,
        center,
        resolution=None,
        normal=None,
        polar=None,
        angle=None,
        tolerance=None,
        progress_bar=False,
    ):
        """Sample a dataset over a circular arc defined by a normal and polar vector and plot it.

        The number of segments composing the polyline is controlled by
        setting the object resolution.

        Parameters
        ----------
        center : sequence[float]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide circular arc into. Defaults to
            number of cells in the input mesh. Must be a positive
            integer.

        normal : sequence[float], optional
            The normal vector to the plane of the arc.  By default it
            points in the positive Z direction.

        polar : sequence[float], optional
            Starting point of the arc in polar coordinates.  By
            default it is the unit vector in the positive x direction.

        angle : float, optional
            Arc length (in degrees), beginning at the polar vector.  The
            direction is counterclockwise.  By default it is 360.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Sampled Dataset.

        Examples
        --------
        Sample a dataset over a circular arc.

        >>> import pyvista
        >>> from pyvista import examples
        >>> uniform = examples.load_uniform()
        >>> uniform["height"] = uniform.points[:, 2]
        >>> normal = [0, 0, 1]
        >>> polar = [0, 9, 0]
        >>> center = [
        ...     uniform.bounds[1],
        ...     uniform.bounds[2],
        ...     uniform.bounds[5],
        ... ]
        >>> arc = uniform.sample_over_circular_arc_normal(
        ...     center, normal=normal, polar=polar
        ... )
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(uniform, style='wireframe')
        >>> _ = pl.add_mesh(arc, line_width=10)
        >>> pl.show_axes()
        >>> pl.show()

        """
        if resolution is None:
            resolution = int(self.n_cells)
        # Make a circular arc and sample the dataset
        circular_arc = pyvista.CircularArcFromNormal(
            center, resolution=resolution, normal=normal, polar=polar, angle=angle
        )
        return circular_arc.sample(self, tolerance=tolerance, progress_bar=progress_bar)

    def plot_over_circular_arc(
        self,
        pointa,
        pointb,
        center,
        resolution=None,
        scalars=None,
        title=None,
        ylabel=None,
        figsize=None,
        figure=True,
        show=True,
        tolerance=None,
        fname=None,
        progress_bar=False,
    ):
        """Sample a dataset along a circular arc and plot it.

        Plot the variables of interest in 2D where the X-axis is
        distance from Point A and the Y-axis is the variable of
        interest. Note that this filter returns ``None``.

        Parameters
        ----------
        pointa : sequence[float]
            Location in ``[x, y, z]``.

        pointb : sequence[float]
            Location in ``[x, y, z]``.

        center : sequence[float]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide the circular arc into. Defaults
            to number of cells in the input mesh. Must be a positive
            integer.

        scalars : str, optional
            The string name of the variable in the input dataset to
            probe. The active scalar is used by default.

        title : str, optional
            The string title of the ``matplotlib`` figure.

        ylabel : str, optional
            The string label of the Y-axis. Defaults to the variable name.

        figsize : tuple(int), optional
            The size of the new figure.

        figure : bool, default: True
            Flag on whether or not to create a new figure.

        show : bool, default: True
            Shows the ``matplotlib`` figure when ``True``.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        fname : str, optional
            Save the figure this file name when set.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Examples
        --------
        Sample a dataset along a high resolution circular arc and plot.

        >>> from pyvista import examples
        >>> mesh = examples.load_uniform()
        >>> a = [mesh.bounds[0], mesh.bounds[2], mesh.bounds[5]]
        >>> b = [mesh.bounds[1], mesh.bounds[2], mesh.bounds[4]]
        >>> center = [mesh.bounds[0], mesh.bounds[2], mesh.bounds[4]]
        >>> mesh.plot_over_circular_arc(
        ...     a, b, center, resolution=1000, show=False
        ... )  # doctest:+SKIP

        """
        # Sample on circular arc
        sampled = DataSetFilters.sample_over_circular_arc(
            self, pointa, pointb, center, resolution, tolerance, progress_bar=progress_bar
        )

        # Get variable of interest
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        values = sampled.get_array(scalars)
        distance = sampled['Distance']

        # create the matplotlib figure
        if figure:
            plt.figure(figsize=figsize)
        # Plot it in 2D
        if values.ndim > 1:
            for i in range(values.shape[1]):
                plt.plot(distance, values[:, i], label=f'Component {i}')
            plt.legend()
        else:
            plt.plot(distance, values)
        plt.xlabel('Distance')
        if ylabel is None:
            plt.ylabel(scalars)
        else:
            plt.ylabel(ylabel)
        if title is None:
            plt.title(f'{scalars} Profile')
        else:
            plt.title(title)
        if fname:
            plt.savefig(fname)
        if show:  # pragma: no cover
            plt.show()

    def plot_over_circular_arc_normal(
        self,
        center,
        resolution=None,
        normal=None,
        polar=None,
        angle=None,
        scalars=None,
        title=None,
        ylabel=None,
        figsize=None,
        figure=True,
        show=True,
        tolerance=None,
        fname=None,
        progress_bar=False,
    ):
        """Sample a dataset along a resolution circular arc defined by a normal and polar vector and plot it.

        Plot the variables of interest in 2D where the X-axis is
        distance from Point A and the Y-axis is the variable of
        interest. Note that this filter returns ``None``.

        Parameters
        ----------
        center : sequence[int]
            Location in ``[x, y, z]``.

        resolution : int, optional
            Number of pieces to divide circular arc into. Defaults to
            number of cells in the input mesh. Must be a positive
            integer.

        normal : sequence[float], optional
            The normal vector to the plane of the arc.  By default it
            points in the positive Z direction.

        polar : sequence[float], optional
            Starting point of the arc in polar coordinates.  By
            default it is the unit vector in the positive x direction.

        angle : float, optional
            Arc length (in degrees), beginning at the polar vector.  The
            direction is counterclockwise.  By default it is 360.

        scalars : str, optional
            The string name of the variable in the input dataset to
            probe. The active scalar is used by default.

        title : str, optional
            The string title of the `matplotlib` figure.

        ylabel : str, optional
            The string label of the Y-axis. Defaults to variable name.

        figsize : tuple(int), optional
            The size of the new figure.

        figure : bool, optional
            Flag on whether or not to create a new figure.

        show : bool, default: True
            Shows the matplotlib figure.

        tolerance : float, optional
            Tolerance used to compute whether a point in the source is
            in a cell of the input.  If not given, tolerance is
            automatically generated.

        fname : str, optional
            Save the figure this file name when set.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Examples
        --------
        Sample a dataset along a high resolution circular arc and plot.

        >>> from pyvista import examples
        >>> mesh = examples.load_uniform()
        >>> normal = normal = [0, 0, 1]
        >>> polar = [0, 9, 0]
        >>> angle = 90
        >>> center = [mesh.bounds[0], mesh.bounds[2], mesh.bounds[4]]
        >>> mesh.plot_over_circular_arc_normal(
        ...     center, polar=polar, angle=angle
        ... )  # doctest:+SKIP

        """
        # Sample on circular arc
        sampled = DataSetFilters.sample_over_circular_arc_normal(
            self, center, resolution, normal, polar, angle, tolerance, progress_bar=progress_bar
        )

        # Get variable of interest
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        values = sampled.get_array(scalars)
        distance = sampled['Distance']

        # create the matplotlib figure
        if figure:
            plt.figure(figsize=figsize)
        # Plot it in 2D
        if values.ndim > 1:
            for i in range(values.shape[1]):
                plt.plot(distance, values[:, i], label=f'Component {i}')
            plt.legend()
        else:
            plt.plot(distance, values)
        plt.xlabel('Distance')
        if ylabel is None:
            plt.ylabel(scalars)
        else:
            plt.ylabel(ylabel)
        if title is None:
            plt.title(f'{scalars} Profile')
        else:
            plt.title(title)
        if fname:
            plt.savefig(fname)
        if show:  # pragma: no cover
            plt.show()

    def extract_cells(self, ind, invert=False, progress_bar=False):
        """Return a subset of the grid.

        Parameters
        ----------
        ind : sequence[int]
            Numpy array of cell indices to be extracted.

        invert : bool, default: False
            Invert the selection.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Subselected grid.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> grid = pyvista.read(examples.hexbeamfile)
        >>> subset = grid.extract_cells(range(20))
        >>> subset.n_cells
        20
        >>> pl = pyvista.Plotter()
        >>> actor = pl.add_mesh(
        ...     grid, style='wireframe', line_width=5, color='black'
        ... )
        >>> actor = pl.add_mesh(subset, color='grey')
        >>> pl.show()

        """
        if invert:
            _, ind = numpy_to_idarr(ind, return_ind=True)
            ind = [i for i in range(self.n_cells) if i not in ind]

        # Create selection objects
        selectionNode = _vtk.vtkSelectionNode()
        selectionNode.SetFieldType(_vtk.vtkSelectionNode.CELL)
        selectionNode.SetContentType(_vtk.vtkSelectionNode.INDICES)
        selectionNode.SetSelectionList(numpy_to_idarr(ind))

        selection = _vtk.vtkSelection()
        selection.AddNode(selectionNode)

        # extract
        extract_sel = _vtk.vtkExtractSelection()
        extract_sel.SetInputData(0, self)
        extract_sel.SetInputData(1, selection)
        _update_alg(extract_sel, progress_bar, 'Extracting Cells')
        subgrid = _get_output(extract_sel)

        # extracts only in float32
        if subgrid.n_points:
            if self.points.dtype != np.dtype('float32'):
                ind = subgrid.point_data['vtkOriginalPointIds']
                subgrid.points = self.points[ind]

        return subgrid

    def extract_points(self, ind, adjacent_cells=True, include_cells=True, progress_bar=False):
        """Return a subset of the grid (with cells) that contains any of the given point indices.

        Parameters
        ----------
        ind : sequence[int]
            Sequence of point indices to be extracted.

        adjacent_cells : bool, default: True
            If ``True``, extract the cells that contain at least one of
            the extracted points. If ``False``, extract the cells that
            contain exclusively points from the extracted points list.

        include_cells : bool, default: True
            Specifies if the cells shall be returned or not.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Subselected grid.

        Examples
        --------
        Extract all the points of a sphere with a Z coordinate greater than 0

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> extracted = sphere.extract_points(sphere.points[:, 2] > 0)
        >>> extracted.clear_data()  # clear for plotting
        >>> extracted.plot()

        """
        # Create selection objects
        selectionNode = _vtk.vtkSelectionNode()
        selectionNode.SetFieldType(_vtk.vtkSelectionNode.POINT)
        selectionNode.SetContentType(_vtk.vtkSelectionNode.INDICES)
        if not include_cells:
            adjacent_cells = True
        if not adjacent_cells:
            # Build array of point indices to be removed.
            ind_rem = np.ones(self.n_points, dtype='bool')
            ind_rem[ind] = False
            ind = np.arange(self.n_points)[ind_rem]
            # Invert selection
            selectionNode.GetProperties().Set(_vtk.vtkSelectionNode.INVERSE(), 1)
        selectionNode.SetSelectionList(numpy_to_idarr(ind))
        if include_cells:
            selectionNode.GetProperties().Set(_vtk.vtkSelectionNode.CONTAINING_CELLS(), 1)

        selection = _vtk.vtkSelection()
        selection.AddNode(selectionNode)

        # extract
        extract_sel = _vtk.vtkExtractSelection()
        extract_sel.SetInputData(0, self)
        extract_sel.SetInputData(1, selection)
        _update_alg(extract_sel, progress_bar, 'Extracting Points')
        return _get_output(extract_sel)

    def extract_surface(
        self, pass_pointid=True, pass_cellid=True, nonlinear_subdivision=1, progress_bar=False
    ):
        """Extract surface mesh of the grid.

        Parameters
        ----------
        pass_pointid : bool, default: True
            Adds a point array ``"vtkOriginalPointIds"`` that
            idenfities which original points these surface points
            correspond to.

        pass_cellid : bool, default: True
            Adds a cell array ``"vtkOriginalPointIds"`` that
            idenfities which original cells these surface cells
            correspond to.

        nonlinear_subdivision : int, default: 1
            If the input is an unstructured grid with nonlinear faces,
            this parameter determines how many times the face is
            subdivided into linear faces.

            If 0, the output is the equivalent of its linear
            counterpart (and the midpoints determining the nonlinear
            interpolation are discarded). If 1 (the default), the
            nonlinear face is triangulated based on the midpoints. If
            greater than 1, the triangulated pieces are recursively
            subdivided to reach the desired subdivision. Setting the
            value to greater than 1 may cause some point data to not
            be passed even if no nonlinear faces exist. This option
            has no effect if the input is not an unstructured grid.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Surface mesh of the grid.

        Examples
        --------
        Extract the surface of an UnstructuredGrid.

        >>> import pyvista
        >>> from pyvista import examples
        >>> grid = examples.load_hexbeam()
        >>> surf = grid.extract_surface()
        >>> type(surf)
        <class 'pyvista.core.pointset.PolyData'>

        See the :ref:`extract_surface_example` for more examples using this filter.

        """
        surf_filter = _vtk.vtkDataSetSurfaceFilter()
        surf_filter.SetInputData(self)
        surf_filter.SetPassThroughPointIds(pass_pointid)
        surf_filter.SetPassThroughCellIds(pass_cellid)

        if nonlinear_subdivision != 1:
            surf_filter.SetNonlinearSubdivisionLevel(nonlinear_subdivision)

        # available in 9.0.2
        # surf_filter.SetDelegation(delegation)

        _update_alg(surf_filter, progress_bar, 'Extracting Surface')
        return _get_output(surf_filter)

    def surface_indices(self, progress_bar=False):
        """Return the surface indices of a grid.

        Parameters
        ----------
        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        numpy.ndarray
            Indices of the surface points.

        Examples
        --------
        Return the first 10 surface indices of an UnstructuredGrid.

        >>> from pyvista import examples
        >>> grid = examples.load_hexbeam()
        >>> ind = grid.surface_indices()
        >>> ind[:10]  # doctest:+SKIP
        pyvista_ndarray([ 0,  2, 36, 27,  7,  8, 81,  1, 18,  4])

        """
        surf = DataSetFilters.extract_surface(self, pass_cellid=True, progress_bar=progress_bar)
        return surf.point_data['vtkOriginalPointIds']

    def extract_feature_edges(
        self,
        feature_angle=30.0,
        boundary_edges=True,
        non_manifold_edges=True,
        feature_edges=True,
        manifold_edges=True,
        clear_data=False,
        progress_bar=False,
    ):
        """Extract edges from the surface of the mesh.

        If the given mesh is not PolyData, the external surface of the given
        mesh is extracted and used.

        From vtk documentation, the edges are one of the following:

            1) Boundary (used by one polygon) or a line cell.
            2) Non-manifold (used by three or more polygons).
            3) Feature edges (edges used by two triangles and whose
               dihedral angle > feature_angle).
            4) Manifold edges (edges used by exactly two polygons).

        Parameters
        ----------
        feature_angle : float, default: 30.0
            Feature angle (in degrees) used to detect sharp edges on
            the mesh. Used only when ``feature_edges=True``.

        boundary_edges : bool, default: True
            Extract the boundary edges.

        non_manifold_edges : bool, default: True
            Extract non-manifold edges.

        feature_edges : bool, default: True
            Extract edges exceeding ``feature_angle``.

        manifold_edges : bool, default: True
            Extract manifold edges.

        clear_data : bool, default: False
            Clear any point, cell, or field data. This is useful
            if wanting to strictly extract the edges.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Extracted edges.

        Examples
        --------
        Extract the edges from an unstructured grid.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> feat_edges = hex_beam.extract_feature_edges()
        >>> feat_edges.clear_data()  # clear array data for plotting
        >>> feat_edges.plot(line_width=10)

        See the :ref:`extract_edges_example` for more examples using this filter.

        """
        dataset = self
        if not isinstance(dataset, _vtk.vtkPolyData):
            dataset = DataSetFilters.extract_surface(dataset)
        featureEdges = _vtk.vtkFeatureEdges()
        featureEdges.SetInputData(dataset)
        featureEdges.SetFeatureAngle(feature_angle)
        featureEdges.SetManifoldEdges(manifold_edges)
        featureEdges.SetNonManifoldEdges(non_manifold_edges)
        featureEdges.SetBoundaryEdges(boundary_edges)
        featureEdges.SetFeatureEdges(feature_edges)
        featureEdges.SetColoring(False)
        _update_alg(featureEdges, progress_bar, 'Extracting Feature Edges')
        output = _get_output(featureEdges)
        if clear_data:
            output.clear_data()
        return output

    def merge(
        self,
        grid=None,
        merge_points=True,
        tolerance=0.0,
        inplace=False,
        main_has_priority=True,
        progress_bar=False,
    ):
        """Join one or many other grids to this grid.

        Grid is updated in-place by default.

        Can be used to merge points of adjacent cells when no grids
        are input.

        .. note::
           The ``+`` operator between two meshes uses this filter with
           the default parameters. When the target mesh is already a
           :class:`pyvista.UnstructuredGrid`, in-place merging via
           ``+=`` is similarly possible.

        Parameters
        ----------
        grid : vtk.UnstructuredGrid or list of vtk.UnstructuredGrids, optional
            Grids to merge to this grid.

        merge_points : bool, default: True
            Points in exactly the same location will be merged between
            the two meshes. Warning: this can leave degenerate point data.

        tolerance : float, default: 0.0
            The absolute tolerance to use to find coincident points when
            ``merge_points=True``.

        inplace : bool, default: False
            Updates grid inplace when True if the input type is an
            :class:`pyvista.UnstructuredGrid`.

        main_has_priority : bool, default: True
            When this parameter is true and merge_points is true,
            the arrays of the merging grids will be overwritten
            by the original main mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Merged grid.

        Notes
        -----
        When two or more grids are joined, the type and name of each
        array must match or the arrays will be ignored and not
        included in the final merged mesh.

        Examples
        --------
        Merge three separate spheres into a single mesh.

        >>> import pyvista
        >>> sphere_a = pyvista.Sphere(center=(1, 0, 0))
        >>> sphere_b = pyvista.Sphere(center=(0, 1, 0))
        >>> sphere_c = pyvista.Sphere(center=(0, 0, 1))
        >>> merged = sphere_a.merge([sphere_b, sphere_c])
        >>> merged.plot()

        """
        append_filter = _vtk.vtkAppendFilter()
        append_filter.SetMergePoints(merge_points)
        append_filter.SetTolerance(tolerance)

        if not main_has_priority:
            append_filter.AddInputData(self)

        if isinstance(grid, pyvista.DataSet):
            append_filter.AddInputData(grid)
        elif isinstance(grid, (list, tuple, pyvista.MultiBlock)):
            grids = grid
            for grid in grids:
                append_filter.AddInputData(grid)

        if main_has_priority:
            append_filter.AddInputData(self)

        _update_alg(append_filter, progress_bar, 'Merging')
        merged = _get_output(append_filter)
        if inplace:
            if type(self) == type(merged):
                self.deep_copy(merged)
                return self
            else:
                raise TypeError(f"Mesh type {type(self)} cannot be overridden by output.")
        return merged

    def __add__(self, dataset):
        """Combine this mesh with another into a :class:`pyvista.UnstructuredGrid`."""
        return DataSetFilters.merge(self, dataset)

    def __iadd__(self, dataset):
        """Merge another mesh into this one if possible.

        "If possible" means that ``self`` is a :class:`pyvista.UnstructuredGrid`.
        Otherwise we have to return a new object, and the attempted in-place
        merge will raise.

        """
        try:
            merged = DataSetFilters.merge(self, dataset, inplace=True)
        except TypeError:
            raise TypeError(
                'In-place merge only possible if the target mesh '
                'is an UnstructuredGrid.\nPlease use `mesh + other_mesh` '
                'instead, which returns a new UnstructuredGrid.'
            ) from None
        return merged

    def compute_cell_quality(
        self, quality_measure='scaled_jacobian', null_value=-1.0, progress_bar=False
    ):
        """Compute a function of (geometric) quality for each cell of a mesh.

        The per-cell quality is added to the mesh's cell data, in an
        array named ``"CellQuality"``. Cell types not supported by this
        filter or undefined quality of supported cell types will have an
        entry of -1.

        Defaults to computing the scaled Jacobian.

        Options for cell quality measure:

        - ``'area'``
        - ``'aspect_beta'``
        - ``'aspect_frobenius'``
        - ``'aspect_gamma'``
        - ``'aspect_ratio'``
        - ``'collapse_ratio'``
        - ``'condition'``
        - ``'diagonal'``
        - ``'dimension'``
        - ``'distortion'``
        - ``'jacobian'``
        - ``'max_angle'``
        - ``'max_aspect_frobenius'``
        - ``'max_edge_ratio'``
        - ``'med_aspect_frobenius'``
        - ``'min_angle'``
        - ``'oddy'``
        - ``'radius_ratio'``
        - ``'relative_size_squared'``
        - ``'scaled_jacobian'``
        - ``'shape'``
        - ``'shape_and_size'``
        - ``'shear'``
        - ``'shear_and_size'``
        - ``'skew'``
        - ``'stretch'``
        - ``'taper'``
        - ``'volume'``
        - ``'warpage'``

        Parameters
        ----------
        quality_measure : str, default: 'scaled_jacobian'
            The cell quality measure to use.

        null_value : float, default: -1.0
            Float value for undefined quality. Undefined quality are qualities
            that could be addressed by this filter but is not well defined for
            the particular geometry of cell in question, e.g. a volume query
            for a triangle. Undefined quality will always be undefined.
            The default value is -1.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with the computed mesh quality in the
            ``cell_data`` as the ``"CellQuality"`` array.

        Examples
        --------
        Compute and plot the minimum angle of a sample sphere mesh.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(theta_resolution=20, phi_resolution=20)
        >>> cqual = sphere.compute_cell_quality('min_angle')
        >>> cqual.plot(show_edges=True)

        See the :ref:`mesh_quality_example` for more examples using this filter.

        """
        alg = _vtk.vtkCellQuality()
        possible_measure_setters = {
            'area': 'SetQualityMeasureToArea',
            'aspect_beta': 'SetQualityMeasureToAspectBeta',
            'aspect_frobenius': 'SetQualityMeasureToAspectFrobenius',
            'aspect_gamma': 'SetQualityMeasureToAspectGamma',
            'aspect_ratio': 'SetQualityMeasureToAspectRatio',
            'collapse_ratio': 'SetQualityMeasureToCollapseRatio',
            'condition': 'SetQualityMeasureToCondition',
            'diagonal': 'SetQualityMeasureToDiagonal',
            'dimension': 'SetQualityMeasureToDimension',
            'distortion': 'SetQualityMeasureToDistortion',
            'jacobian': 'SetQualityMeasureToJacobian',
            'max_angle': 'SetQualityMeasureToMaxAngle',
            'max_aspect_frobenius': 'SetQualityMeasureToMaxAspectFrobenius',
            'max_edge_ratio': 'SetQualityMeasureToMaxEdgeRatio',
            'med_aspect_frobenius': 'SetQualityMeasureToMedAspectFrobenius',
            'min_angle': 'SetQualityMeasureToMinAngle',
            'oddy': 'SetQualityMeasureToOddy',
            'radius_ratio': 'SetQualityMeasureToRadiusRatio',
            'relative_size_squared': 'SetQualityMeasureToRelativeSizeSquared',
            'scaled_jacobian': 'SetQualityMeasureToScaledJacobian',
            'shape': 'SetQualityMeasureToShape',
            'shape_and_size': 'SetQualityMeasureToShapeAndSize',
            'shear': 'SetQualityMeasureToShear',
            'shear_and_size': 'SetQualityMeasureToShearAndSize',
            'skew': 'SetQualityMeasureToSkew',
            'stretch': 'SetQualityMeasureToStretch',
            'taper': 'SetQualityMeasureToTaper',
            'volume': 'SetQualityMeasureToVolume',
            'warpage': 'SetQualityMeasureToWarpage',
        }

        # we need to check if these quality measures exist as VTK API changes
        measure_setters = {}
        for name, attr in possible_measure_setters.items():
            setter_candidate = getattr(alg, attr, None)
            if setter_candidate:
                measure_setters[name] = setter_candidate

        try:
            # Set user specified quality measure
            measure_setters[quality_measure]()
        except (KeyError, IndexError):
            options = ', '.join([f"'{s}'" for s in list(measure_setters.keys())])
            raise KeyError(
                f'Cell quality type ({quality_measure}) not available. Options are: {options}'
            )
        alg.SetInputData(self)
        alg.SetUndefinedQuality(null_value)
        _update_alg(alg, progress_bar, 'Computing Cell Quality')
        return _get_output(alg)

    def compute_derivative(
        self,
        scalars=None,
        gradient=True,
        divergence=None,
        vorticity=None,
        qcriterion=None,
        faster=False,
        preference='point',
        progress_bar=False,
    ):
        """Compute derivative-based quantities of point/cell scalar field.

        Utilize ``vtkGradientFilter`` to compute derivative-based quantities,
        such as gradient, divergence, vorticity, and Q-criterion, of the
        selected point or cell scalar field.

        Parameters
        ----------
        scalars : str, optional
            String name of the scalars array to use when computing the
            derivative quantities.  Defaults to the active scalars in
            the dataset.

        gradient : bool | str, default: True
            Calculate gradient. If a string is passed, the string will be used
            for the resulting array name. Otherwise, array name will be
            ``'gradient'``. Default ``True``.

        divergence : bool | str, optional
            Calculate divergence. If a string is passed, the string will be
            used for the resulting array name. Otherwise, default array name
            will be ``'divergence'``.

        vorticity : bool | str, optional
            Calculate vorticity. If a string is passed, the string will be used
            for the resulting array name. Otherwise, default array name will be
            ``'vorticity'``.

        qcriterion : bool | str, optional
            Calculate qcriterion. If a string is passed, the string will be
            used for the resulting array name. Otherwise, default array name
            will be ``'qcriterion'``.

        faster : bool, default: False
            Use faster algorithm for computing derivative quantities. Result is
            less accurate and performs fewer derivative calculations,
            increasing computation speed. The error will feature smoothing of
            the output and possibly errors at boundaries. Option has no effect
            if DataSet is not :class:`pyvista.UnstructuredGrid`.

        preference : str, default: "point"
            Data type preference. Either ``'point'`` or ``'cell'``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with calculated derivative.

        Examples
        --------
        First, plot the random hills dataset with the active elevation
        scalars.  These scalars will be used for the derivative
        calculations.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> hills.plot(smooth_shading=True)

        Compute and plot the gradient of the active scalars.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> deriv = hills.compute_derivative()
        >>> deriv.plot(scalars='gradient')

        See the :ref:`gradients_example` for more examples using this filter.

        """
        alg = _vtk.vtkGradientFilter()
        # Check if scalars array given
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        if not isinstance(scalars, str):
            raise TypeError('scalars array must be given as a string name')
        if not any((gradient, divergence, vorticity, qcriterion)):
            raise ValueError(
                'must set at least one of gradient, divergence, vorticity, or qcriterion'
            )

            # bool(non-empty string/True) == True, bool(None/False) == False
        alg.SetComputeGradient(bool(gradient))
        if isinstance(gradient, bool):
            gradient = 'gradient'
        alg.SetResultArrayName(gradient)

        alg.SetComputeDivergence(bool(divergence))
        if isinstance(divergence, bool):
            divergence = 'divergence'
        alg.SetDivergenceArrayName(divergence)

        alg.SetComputeVorticity(bool(vorticity))
        if isinstance(vorticity, bool):
            vorticity = 'vorticity'
        alg.SetVorticityArrayName(vorticity)

        alg.SetComputeQCriterion(bool(qcriterion))
        if isinstance(qcriterion, bool):
            qcriterion = 'qcriterion'
        alg.SetQCriterionArrayName(qcriterion)

        alg.SetFasterApproximation(faster)
        field = get_array_association(self, scalars, preference=preference)
        # args: (idx, port, connection, field, name)
        alg.SetInputArrayToProcess(0, 0, 0, field.value, scalars)
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Computing Derivative')
        return _get_output(alg)

    def shrink(self, shrink_factor=1.0, progress_bar=False):
        """Shrink the individual faces of a mesh.

        This filter shrinks the individual faces of a mesh rather than
        scaling the entire mesh.

        Parameters
        ----------
        shrink_factor : float, default: 1.0
            Fraction of shrink for each cell. Default does not modify the
            faces.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with shrunk faces.  Return type matches input.

        Examples
        --------
        First, plot the original cube.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.plot(show_edges=True, line_width=5)

        Now, plot the mesh with shrunk faces.

        >>> shrunk = mesh.shrink(0.5)
        >>> shrunk.clear_data()  # cleans up plot
        >>> shrunk.plot(show_edges=True, line_width=5)

        """
        if not (0.0 <= shrink_factor <= 1.0):
            raise ValueError('`shrink_factor` should be between 0.0 and 1.0')
        alg = _vtk.vtkShrinkFilter()
        alg.SetInputData(self)
        alg.SetShrinkFactor(shrink_factor)
        _update_alg(alg, progress_bar, 'Shrinking Mesh')
        output = _get_output(alg)
        if isinstance(self, _vtk.vtkPolyData):
            return output.extract_surface()
        return output

    def tessellate(self, max_n_subdivide=3, merge_points=True, progress_bar=False):
        """Tessellate a mesh.

        This filter approximates nonlinear FEM-like elements with linear
        simplices. The output mesh will have geometry and any fields specified
        as attributes in the input mesh's point data. The attribute's copy
        flags are honored, except for normals.

        For more details see `vtkTessellatorFilter <https://vtk.org/doc/nightly/html/classvtkTessellatorFilter.html#details>`_.

        Parameters
        ----------
        max_n_subdivide : int, default: 3
            Maximum number of subdivisions.

        merge_points : bool, default: True
            The adaptive tessellation will output vertices that are not shared among cells,
            even where they should be. This can be corrected to some extent.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with tessellated mesh.  Return type matches input.

        Examples
        --------
        First, plot the high order FEM-like elements.

        >>> import pyvista
        >>> import numpy as np
        >>> points = np.array(
        ...     [
        ...         [0.0, 0.0, 0.0],
        ...         [2.0, 0.0, 0.0],
        ...         [1.0, 2.0, 0.0],
        ...         [1.0, 0.5, 0.0],
        ...         [1.5, 1.5, 0.0],
        ...         [0.5, 1.5, 0.0],
        ...     ]
        ... )
        >>> cells = np.array([6, 0, 1, 2, 3, 4, 5])
        >>> cell_types = np.array([69])
        >>> mesh = pyvista.UnstructuredGrid(cells, cell_types, points)
        >>> mesh.plot(show_edges=True, line_width=5)

        Now, plot the tessellated mesh.

        >>> tessellated = mesh.tessellate()
        >>> tessellated.clear_data()  # cleans up plot
        >>> tessellated.plot(show_edges=True, line_width=5)

        """
        if isinstance(self, _vtk.vtkPolyData):
            raise TypeError('Tessellate filter is not supported for PolyData objects.')
        alg = _vtk.vtkTessellatorFilter()
        alg.SetInputData(self)
        alg.SetMergePoints(merge_points)
        alg.SetMaximumNumberOfSubdivisions(max_n_subdivide)
        _update_alg(alg, progress_bar, 'Tessellating Mesh')
        output = _get_output(alg)
        return output

    def transform(
        self: _vtk.vtkDataSet,
        trans: Union[_vtk.vtkMatrix4x4, _vtk.vtkTransform, np.ndarray],
        transform_all_input_vectors=False,
        inplace=True,
        progress_bar=False,
    ):
        """Transform this mesh with a 4x4 transform.

        .. warning::
            When using ``transform_all_input_vectors=True``, there is
            no distinction in VTK between vectors and arrays with
            three components.  This may be an issue if you have scalar
            data with three components (e.g. RGB data).  This will be
            improperly transformed as if it was vector data rather
            than scalar data.  One possible (albeit ugly) workaround
            is to store the three components as separate scalar
            arrays.

        .. warning::
            In general, transformations give non-integer results. This
            method converts integer-typed vector data to float before
            performing the transformation. This applies to the points
            array, as well as any vector-valued data that is affected
            by the transformation. To prevent subtle bugs arising from
            in-place transformations truncating the result to integers,
            this conversion always applies to the input mesh.

        Parameters
        ----------
        trans : vtk.vtkMatrix4x4, vtk.vtkTransform, or numpy.ndarray
            Accepts a vtk transformation object or a 4x4
            transformation matrix.

        transform_all_input_vectors : bool, default: False
            When ``True``, all arrays with three components are
            transformed. Otherwise, only the normals and vectors are
            transformed.  See the warning for more details.

        inplace : bool, default: False
            When ``True``, modifies the dataset inplace.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Transformed dataset.  Return type matches input unless
            input dataset is a :class:`pyvista.UniformGrid`, in which
            case the output datatype is a :class:`pyvista.StructuredGrid`.

        Examples
        --------
        Translate a mesh by ``(50, 100, 200)``.

        >>> import numpy as np
        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()

        Here a 4x4 :class:`numpy.ndarray` is used, but
        ``vtk.vtkMatrix4x4`` and ``vtk.vtkTransform`` are also
        accepted.

        >>> transform_matrix = np.array(
        ...     [
        ...         [1, 0, 0, 50],
        ...         [0, 1, 0, 100],
        ...         [0, 0, 1, 200],
        ...         [0, 0, 0, 1],
        ...     ]
        ... )
        >>> transformed = mesh.transform(transform_matrix)
        >>> transformed.plot(show_edges=True)

        """
        if inplace and isinstance(self, pyvista.Grid):
            raise TypeError(f'Cannot transform a {self.__class__} inplace')

        if isinstance(trans, _vtk.vtkMatrix4x4):
            m = trans
            t = _vtk.vtkTransform()
            t.SetMatrix(m)
        elif isinstance(trans, _vtk.vtkTransform):
            t = trans
            m = trans.GetMatrix()
        elif isinstance(trans, np.ndarray):
            if trans.shape != (4, 4):
                raise ValueError('Transformation array must be 4x4')
            m = pyvista.vtkmatrix_from_array(trans)
            t = _vtk.vtkTransform()
            t.SetMatrix(m)
        else:
            raise TypeError(
                'Input transform must be either:\n'
                '\tvtk.vtkMatrix4x4\n'
                '\tvtk.vtkTransform\n'
                '\t4x4 np.ndarray\n'
            )

        if m.GetElement(3, 3) == 0:
            raise ValueError("Transform element (3,3), the inverse scale term, is zero")

        # vtkTransformFilter truncates the result if the input is an integer type
        # so convert input points and relevant vectors to float
        # (creating a new copy would be harmful much more often)
        converted_ints = False
        if not np.issubdtype(self.points.dtype, np.floating):
            self.points = self.points.astype(np.float32)
            converted_ints = True
        if transform_all_input_vectors:
            # all vector-shaped data will be transformed
            point_vectors = [
                name for name, data in self.point_data.items() if data.shape == (self.n_points, 3)
            ]
            cell_vectors = [
                name for name, data in self.cell_data.items() if data.shape == (self.n_cells, 3)
            ]
        else:
            # we'll only transform active vectors and normals
            point_vectors = [
                self.point_data.active_vectors_name,
                self.point_data.active_normals_name,
            ]
            cell_vectors = [
                self.cell_data.active_vectors_name,
                self.cell_data.active_normals_name,
            ]
        # dynamically convert each self.point_data[name] etc. to float32
        all_vectors = [point_vectors, cell_vectors]
        all_dataset_attrs = [self.point_data, self.cell_data]
        for vector_names, dataset_attrs in zip(all_vectors, all_dataset_attrs):
            for vector_name in vector_names:
                if vector_name is None:
                    continue
                vector_arr = dataset_attrs[vector_name]
                if not np.issubdtype(vector_arr.dtype, np.floating):
                    dataset_attrs[vector_name] = vector_arr.astype(np.float32)
                    converted_ints = True
        if converted_ints:
            warnings.warn(
                'Integer points, vector and normal data (if any) of the input mesh '
                'have been converted to ``np.float32``. This is necessary in order '
                'to transform properly.'
            )

        # vtkTransformFilter doesn't respect active scalars.  We need to track this
        active_point_scalars_name = self.point_data.active_scalars_name
        active_cell_scalars_name = self.cell_data.active_scalars_name

        # vtkTransformFilter sometimes doesn't transform all vector arrays
        # when there are active point/cell scalars. Use this workaround
        self.active_scalars_name = None

        f = _vtk.vtkTransformFilter()
        f.SetInputDataObject(self)
        f.SetTransform(t)
        f.SetTransformAllInputVectors(transform_all_input_vectors)

        _update_alg(f, progress_bar, 'Transforming')
        res = pyvista.core.filters._get_output(f)

        # make the previously active scalars active again
        if active_point_scalars_name is not None:
            self.point_data.active_scalars_name = active_point_scalars_name
            res.point_data.active_scalars_name = active_point_scalars_name
        if active_cell_scalars_name is not None:
            self.cell_data.active_scalars_name = active_cell_scalars_name
            res.cell_data.active_scalars_name = active_cell_scalars_name

        if inplace:
            self.copy_from(res, deep=False)
            return self

        # The output from the transform filter contains a shallow copy
        # of the original dataset except for the point arrays.  Here
        # we perform a copy so the two are completely unlinked.
        if isinstance(self, pyvista.Grid):
            output = pyvista.StructuredGrid()
        else:
            output = self.__class__()
        output.copy_from(res, deep=True)
        return output

    def reflect(
        self,
        normal,
        point=None,
        inplace=False,
        transform_all_input_vectors=False,
        progress_bar=False,
    ):
        """Reflect a dataset across a plane.

        Parameters
        ----------
        normal : array_like[float]
            Normal direction for reflection.

        point : array_like[float]
            Point which, along with ``normal``, defines the reflection
            plane. If not specified, this is the origin.

        inplace : bool, default: False
            When ``True``, modifies the dataset inplace.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are transformed. Otherwise,
            only the points, normals and active vectors are transformed.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Reflected dataset.  Return type matches input.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()
        >>> mesh = mesh.reflect((0, 0, 1), point=(0, 0, -100))
        >>> mesh.plot(show_edges=True)

        See the :ref:`ref_reflect_example` for more examples using this filter.

        """
        t = transformations.reflection(normal, point=point)
        return self.transform(
            t,
            transform_all_input_vectors=transform_all_input_vectors,
            inplace=inplace,
            progress_bar=progress_bar,
        )

    def integrate_data(self, progress_bar=False):
        """Integrate point and cell data.

        Area or volume is also provided in point data.

        This filter uses the VTK `vtkIntegrateAttributes
        <https://vtk.org/doc/nightly/html/classvtkIntegrateAttributes.html>`_
        and requires VTK v9.1.0 or newer.

        Parameters
        ----------
        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UnstructuredGrid
            Mesh with 1 point and 1 vertex cell with integrated data in point
            and cell data.

        Examples
        --------
        Integrate data on a sphere mesh.

        >>> import pyvista
        >>> import numpy as np
        >>> sphere = pyvista.Sphere(
        ...     theta_resolution=100, phi_resolution=100
        ... )
        >>> sphere.point_data["data"] = 2 * np.ones(sphere.n_points)
        >>> integrated = sphere.integrate_data()

        There is only 1 point and cell, so access the only value.

        >>> integrated["Area"][0]
        3.14
        >>> integrated["data"][0]
        6.28

        See the :ref:`integrate_example` for more examples using this filter.

        """
        if not hasattr(_vtk, 'vtkIntegrateAttributes'):  # pragma: no cover
            raise VTKVersionError('`integrate_data` requires VTK 9.1.0 or newer.')

        alg = _vtk.vtkIntegrateAttributes()
        alg.SetInputData(self)
        alg.SetDivideAllCellDataByVolume(False)
        _update_alg(alg, progress_bar, 'Integrating Variables')
        return _get_output(alg)

    def partition(self, n_partitions, generate_global_id=False, as_composite=True):
        """Break down input dataset into a requested number of partitions.

        Cells on boundaries are uniquely assigned to each partition without duplication.

        It uses a kdtree implementation that builds balances the cell
        centers among a requested number of partitions. The current implementation
        only supports power-of-2 target partition. If a non-power of two value
        is specified for ``n_partitions``, then the load balancing simply
        uses the power-of-two greater than the requested value

        For more details, see `vtkRedistributeDataSetFilter
        <https://vtk.org/doc/nightly/html/classvtkRedistributeDataSetFilter.html>`_.

        Parameters
        ----------
        n_partitions : int
            Specify the number of partitions to split the input dataset
            into. Current implementation results in a number of partitions equal
            to the power of 2 greater than or equal to the chosen value.

        generate_global_id : bool, default: False
            Generate global cell ids if ``None`` are present in the input.  If
            global cell ids are present in the input then this flag is
            ignored.

            This is stored as ``"vtkGlobalCellIds"`` within the ``cell_data``
            of the output dataset(s).

        as_composite : bool, default: False
            Return the partitioned dataset as a :class:`pyvista.MultiBlock`.

        Returns
        -------
        pyvista.MultiBlock or pyvista.UnstructuredGrid
            UnStructuredGrid if ``as_composite=False`` and MultiBlock when ``True``.

        Examples
        --------
        Partition a simple UniformGrid into a :class:`pyvista.MultiBlock`
        containing each partition.

        >>> import pyvista as pv
        >>> grid = pv.UniformGrid(dimensions=(5, 5, 5))
        >>> out = grid.partition(4, as_composite=True)
        >>> out.plot(multi_colors=True, show_edges=True)

        Partition of the Stanford bunny.

        >>> from pyvista import examples
        >>> mesh = examples.download_bunny()
        >>> out = mesh.partition(4, as_composite=True)
        >>> out.plot(multi_colors=True, cpos='xy')

        """
        # While vtkRedistributeDataSetFilter exists prior to 9.1.0, it doesn't
        # work correctly, returning the wrong number of partitions.
        if pyvista.vtk_version_info < (9, 1, 0):
            raise VTKVersionError('`partition` requires vtk>=9.1.0')  # pragma: no cover
        if not hasattr(_vtk, 'vtkRedistributeDataSetFilter'):
            raise VTKVersionError(
                '`partition` requires vtkRedistributeDataSetFilter, but it '
                f'was not found in VTK {pyvista.vtk_version_info}'
            )  # pragma: no cover

        alg = _vtk.vtkRedistributeDataSetFilter()
        alg.SetInputData(self)
        alg.SetNumberOfPartitions(n_partitions)
        alg.SetPreservePartitionsInOutput(True)
        alg.SetGenerateGlobalCellIds(generate_global_id)
        alg.Update()

        # pyvista does not yet support vtkPartitionedDataSet
        part = alg.GetOutput()
        datasets = [part.GetPartition(ii) for ii in range(part.GetNumberOfPartitions())]
        output = pyvista.MultiBlock(datasets)
        if not as_composite:
            # note, SetPreservePartitionsInOutput does not work correctly in
            # vtk 9.2.0, so instead we set it to True always and simply merge
            # the result. See:
            # https://gitlab.kitware.com/vtk/vtk/-/issues/18632
            return pyvista.merge(list(output), merge_points=False)
        return output

    def explode(self, factor=0.1):
        """Push each individual cell away from the center of the dataset.

        Parameters
        ----------
        factor : float, default: 0.1
            How much each cell will move from the center of the dataset
            relative to its distance from it. Increase this number to push the
            cells farther away.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid containing the exploded cells.

        Notes
        -----
        This is similar to :func:`shrink <pyvista.DataSetFilters.shrink>`
        except that it does not change the size of the cells.

        Examples
        --------
        >>> import numpy as np
        >>> import pyvista as pv
        >>> xrng = np.linspace(0, 1, 3)
        >>> yrng = np.linspace(0, 2, 4)
        >>> zrng = np.linspace(0, 3, 5)
        >>> grid = pv.RectilinearGrid(xrng, yrng, zrng)
        >>> exploded = grid.explode()
        >>> exploded.plot(show_edges=True)

        """
        split = self.separate_cells()
        if not isinstance(split, pyvista.UnstructuredGrid):
            split = split.cast_to_unstructured_grid()

        vec = (split.cell_centers().points - split.center) * factor
        split.points += np.repeat(vec, np.diff(split.offset), axis=0)
        return split

    def separate_cells(self):
        """Return a copy of the dataset with separated cells with no shared points.

        This method may be useful when datasets have scalars that need to be
        associated to each point of each cell rather than either each cell or
        just the points of the dataset.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid with isolated cells.

        Examples
        --------
        Load the example hex beam and separate its cells. This increases the
        total number of points in the dataset since points are no longer
        shared.

        >>> from pyvista import examples
        >>> grid = examples.load_hexbeam()
        >>> grid.n_points
        99
        >>> sep_grid = grid.separate_cells()
        >>> sep_grid.n_points
        320

        See the :ref:`point_cell_scalars_example` for a more detailed example
        using this filter.

        """
        return self.shrink(1.0)

    def extract_cells_by_type(self, cell_types, progress_bar=False):
        """Extract cells of a specified type.

        Given an input dataset and a list of cell types, produce an output
        dataset containing only cells of the specified type(s). Note that if
        the input dataset is homogeneous (e.g., all cells are of the same type)
        and the cell type is one of the cells specified, then the input dataset
        is shallow copied to the output.

        The type of output dataset is always the same as the input type. Since
        structured types of data (i.e., :class:`pyvista.UniformGrid`,
        :class:`pyvista.StructuredGrid`, :class`pyvista.RectilnearGrid`,
        :class:`pyvista.UniformGrid`) are all composed of a cell of the same
        type, the output is either empty, or a shallow copy of the input.
        Unstructured data (:class:`pyvista.UnstructuredGrid`,
        :class:`pyvista.PolyData`) input may produce a subset of the input data
        (depending on the selected cell types).

        Parameters
        ----------
        cell_types :  int | sequence[int]
            The cell types to extract. Must be a single or list of integer cell
            types. See :class:`pyvista.CellType`.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            Dataset with the extracted cells. Type is the same as the input.

        Notes
        -----
        Unlike :func:`pyvista.DataSetFilters.extract_cells` which always
        produces a :class:`pyvista.UnstructuredGrid` output, this filter
        produces the same output type as input type.

        Examples
        --------
        Create an unstructured grid with both hexahedral and tetrahedral
        cells and then extract each individual cell type.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> beam = examples.load_hexbeam()
        >>> beam = beam.translate([1, 0, 0])
        >>> ugrid = beam + examples.load_tetbeam()
        >>> hex_cells = ugrid.extract_cells_by_type(pv.CellType.HEXAHEDRON)
        >>> tet_cells = ugrid.extract_cells_by_type(pv.CellType.TETRA)
        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_text('Extracted Hexahedron cells')
        >>> _ = pl.add_mesh(hex_cells, show_edges=True)
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_text('Extracted Tetrahedron cells')
        >>> _ = pl.add_mesh(tet_cells, show_edges=True)
        >>> pl.show()

        """
        alg = _vtk.vtkExtractCellsByType()
        alg.SetInputDataObject(self)
        if isinstance(cell_types, int):
            alg.AddCellType(cell_types)
        elif isinstance(cell_types, (np.ndarray, collections.abc.Sequence)):
            for cell_type in cell_types:
                alg.AddCellType(cell_type)
        else:
            raise TypeError(
                f'Invalid type {type(cell_types)} for `cell_types`. Expecting an int or a sequence.'
            )
        _update_alg(alg, progress_bar, 'Extracting cell types')
        return _get_output(alg)


def _set_threshold_limit(alg, value, method, invert):
    """Set vtkThreshold limits and function.

    Addresses VTK API deprecations and previous PyVista inconsistencies with ParaView. Reference:

    * https://github.com/pyvista/pyvista/issues/2850
    * https://github.com/pyvista/pyvista/issues/3610
    * https://discourse.vtk.org/t/unnecessary-vtk-api-change/9929

    """
    # Check value
    if isinstance(value, (np.ndarray, collections.abc.Sequence)):
        if len(value) != 2:
            raise ValueError(
                f'Value range must be length one for a float value or two for min/max; not ({value}).'
            )
        # Check range
        if value[0] > value[1]:
            raise ValueError(
                'Value sequence is invalid, please use (min, max). The provided first value is greater than the second.'
            )
    elif isinstance(value, collections.abc.Iterable):
        raise TypeError('Value must either be a single scalar or a sequence.')
    alg.SetInvert(invert)
    # Set values and function
    if pyvista.vtk_version_info >= (9, 1):
        if isinstance(value, (np.ndarray, collections.abc.Sequence)):
            alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_BETWEEN)
            alg.SetLowerThreshold(value[0])
            alg.SetUpperThreshold(value[1])
        else:
            # Single value
            if method.lower() == 'lower':
                alg.SetLowerThreshold(value)
                alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_LOWER)
            elif method.lower() == 'upper':
                alg.SetUpperThreshold(value)
                alg.SetThresholdFunction(_vtk.vtkThreshold.THRESHOLD_UPPER)
            else:
                raise ValueError('Invalid method choice. Either `lower` or `upper`')
    else:  # pragma: no cover
        # ThresholdByLower, ThresholdByUpper, ThresholdBetween
        if isinstance(value, (np.ndarray, collections.abc.Sequence)):
            alg.ThresholdBetween(value[0], value[1])
        else:
            # Single value
            if method.lower() == 'lower':
                alg.ThresholdByLower(value)
            elif method.lower() == 'upper':
                alg.ThresholdByUpper(value)
            else:
                raise ValueError('Invalid method choice. Either `lower` or `upper`')


# pyvista/core/filters/poly_data.py

"""Filters module with a class to manage filters/algorithms for polydata datasets."""
import collections.abc
import warnings

import numpy as np

import pyvista
from pyvista import (
    NORMALS,
    _vtk,
    abstract_class,
    assert_empty_kwargs,
    generate_plane,
    get_array_association,
    vtk_id_list_to_array,
)
from pyvista.core.errors import DeprecationError, NotAllTrianglesError, VTKVersionError
from pyvista.core.filters import _get_output, _update_alg
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.errors import MissingDataError
from pyvista.utilities import FieldAssociation, get_array
from pyvista.utilities.misc import PyVistaFutureWarning


@abstract_class
class PolyDataFilters(DataSetFilters):
    """An internal class to manage filters/algorithms for polydata datasets."""

    def edge_mask(self, angle, progress_bar=False):
        """Return a mask of the points of a surface mesh that has a surface angle greater than angle.

        Parameters
        ----------
        angle : float
            Angle to consider an edge.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        numpy.ndarray
            Mask of points with an angle greater than ``angle``.

        Examples
        --------
        Plot the mask of points that exceed 45 degrees.

        >>> import pyvista
        >>> mesh = pyvista.Cube().triangulate().subdivide(4)
        >>> mask = mesh.edge_mask(45)
        >>> mesh.plot(scalars=mask)

        Show the array of masked points.

        >>> mask  # doctest:+SKIP
        array([ True,  True,  True, ..., False, False, False])

        """
        poly_data = self
        if not isinstance(poly_data, pyvista.PolyData):  # pragma: no cover
            poly_data = pyvista.PolyData(poly_data)
        poly_data.point_data['point_ind'] = np.arange(poly_data.n_points)
        featureEdges = _vtk.vtkFeatureEdges()
        featureEdges.SetInputData(poly_data)
        featureEdges.FeatureEdgesOn()
        featureEdges.BoundaryEdgesOff()
        featureEdges.NonManifoldEdgesOff()
        featureEdges.ManifoldEdgesOff()
        featureEdges.SetFeatureAngle(angle)
        _update_alg(featureEdges, progress_bar, 'Computing Edges')
        edges = _get_output(featureEdges)
        orig_id = pyvista.point_array(edges, 'point_ind')

        return np.in1d(poly_data.point_data['point_ind'], orig_id, assume_unique=True)

    def _boolean(self, btype, other_mesh, tolerance, progress_bar=False):
        """Perform boolean operation."""
        if not isinstance(other_mesh, pyvista.PolyData):
            raise TypeError("Input mesh must be PolyData.")
        if not self.is_all_triangles or not other_mesh.is_all_triangles:
            raise NotAllTrianglesError("Make sure both the input and output are triangulated.")

        bfilter = _vtk.vtkBooleanOperationPolyDataFilter()
        if btype == 'union':
            bfilter.SetOperationToUnion()
        elif btype == 'intersection':
            bfilter.SetOperationToIntersection()
        elif btype == 'difference':
            bfilter.SetOperationToDifference()
        else:  # pragma: no cover
            raise ValueError(f'Invalid btype {btype}')
        bfilter.SetInputData(0, self)
        bfilter.SetInputData(1, other_mesh)
        bfilter.ReorientDifferenceCellsOn()  # this is already default
        bfilter.SetTolerance(tolerance)
        _update_alg(bfilter, progress_bar, 'Performing Boolean Operation')

        return _get_output(bfilter)

    def boolean_cut(self, *args, **kwargs):  # pragma: no cover
        """Cut two meshes.

        .. deprecated:: 0.32.0
           Use :func:`PolyDataFilters.boolean_difference` instead.

        """
        raise DeprecationError(
            '``boolean_cut`` has been deprecated.  Please use ``boolean_difference``.'
        )

    def boolean_add(self, *args, **kwargs):  # pragma: no cover
        """Merge two meshes together.

        .. deprecated:: 0.32.0
           Use :func:`PolyDataFilters.merge` instead.

        """
        raise DeprecationError('``boolean_add`` has been deprecated.  ' 'Please use ``merge``.')

    def boolean_union(self, other_mesh, tolerance=1e-5, progress_bar=False):
        """Perform a boolean union operation on two meshes.

        Essentially, boolean union, difference, and intersection are
        all the same operation. Just different parts of the objects
        are kept at the end.

        The union of two manifold meshes ``A`` and ``B`` is the mesh
        which is in ``A``, in ``B``, or in both ``A`` and ``B``.

        .. note::
           If your boolean operations don't react the way you think they
           should (i.e. the wrong parts disappear), one of your meshes
           probably has its normals pointing inward. Use
           :func:`PolyDataFilters.plot_normals` to visualize the
           normals.

        .. note::
           The behavior of this filter varies from the
           :func:`PolyDataFilters.merge` filter.  This filter attempts
           to create a manifold mesh and will not include internal
           surfaces when two meshes overlap.

        .. note::
           Both meshes must be composed of all triangles.  Check with
           :attr:`PolyData.is_all_triangles` and convert with
           :func:`PolyDataFilters.triangulate`.

        .. versionchanged:: 0.32.0
           Behavior changed to match default VTK behavior.

        Parameters
        ----------
        other_mesh : pyvista.PolyData
            Mesh operating on the source mesh.

        tolerance : float, tolerance: 1e-5
            Tolerance used to determine when a point's absolute
            distance is considered to be zero.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The result of the boolean operation.

        Examples
        --------
        Demonstrate a boolean union with two spheres.  Note how the
        final mesh includes both spheres.

        >>> import pyvista
        >>> sphere_a = pyvista.Sphere()
        >>> sphere_b = pyvista.Sphere(center=(0.5, 0, 0))
        >>> result = sphere_a.boolean_union(sphere_b)
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     sphere_a, color='r', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(
        ...     sphere_b, color='b', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(result, color='tan')
        >>> pl.camera_position = 'xz'
        >>> pl.show()

        See :ref:`boolean_example` for more examples using this filter.

        """
        return self._boolean('union', other_mesh, tolerance, progress_bar=progress_bar)

    def boolean_intersection(self, other_mesh, tolerance=1e-5, progress_bar=False):
        """Perform a boolean intersection operation on two meshes.

        Essentially, boolean union, difference, and intersection are
        all the same operation. Just different parts of the objects
        are kept at the end.

        The intersection of two manifold meshes ``A`` and ``B`` is the mesh
        which is the volume of ``A`` that is also in ``B``.

        .. note::
           If your boolean operations don't react the way you think they
           should (i.e. the wrong parts disappear), one of your meshes
           probably has its normals pointing inward. Use
           :func:`PolyDataFilters.plot_normals` to visualize the
           normals.

        .. note::
           This method returns the "volume" intersection between two
           meshes whereas the :func:`PolyDataFilters.intersection`
           filter returns the surface intersection between two meshes
           (which often resolves as a line).


        .. note::
           Both meshes must be composed of all triangles.  Check with
           :attr:`PolyData.is_all_triangles` and convert with
           :func:`PolyDataFilters.triangulate`.

        .. versionadded:: 0.32.0

        Parameters
        ----------
        other_mesh : pyvista.PolyData
            Mesh operating on the source mesh.

        tolerance : float, default: 1e-5
            Tolerance used to determine when a point's absolute
            distance is considered to be zero.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The result of the boolean operation.

        Examples
        --------
        Demonstrate a boolean intersection with two spheres.  Note how
        the final mesh only includes the intersection of the two.

        >>> import pyvista
        >>> sphere_a = pyvista.Sphere()
        >>> sphere_b = pyvista.Sphere(center=(0.5, 0, 0))
        >>> result = sphere_a.boolean_intersection(sphere_b)
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     sphere_a, color='r', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(
        ...     sphere_b, color='b', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(result, color='tan')
        >>> pl.camera_position = 'xz'
        >>> pl.show()

        See :ref:`boolean_example` for more examples using this filter.

        """
        bool_inter = self._boolean('intersection', other_mesh, tolerance, progress_bar=progress_bar)

        # check if a polydata is completely contained within another
        if bool_inter.n_points == 0:
            inter, s1, s2 = self.intersection(other_mesh)
            if inter.n_points == 0 and s1.n_points == 0 and s2.n_points == 0:
                warnings.warn(
                    'Unable to compute boolean intersection when one PolyData is '
                    'contained within another and no faces intersect.',
                )
        return bool_inter

    def boolean_difference(self, other_mesh, tolerance=1e-5, progress_bar=False):
        """Perform a boolean difference operation between two meshes.

        Essentially, boolean union, difference, and intersection are
        all the same operation. Just different parts of the objects
        are kept at the end.

        The difference of two manifold meshes ``A`` and ``B`` is the
        volume of the mesh in ``A`` not belonging to ``B``.

        .. note::
           If your boolean operations don't react the way you think they
           should (i.e. the wrong parts disappear), one of your meshes
           probably has its normals pointing inward. Use
           :func:`PolyDataFilters.plot_normals` to visualize the
           normals.

        .. note::
           Both meshes must be composed of all triangles.  Check with
           :attr:`PolyData.is_all_triangles` and convert with
           :func:`PolyDataFilters.triangulate`.

        .. versionchanged:: 0.32.0
           Behavior changed to match default VTK behavior.

        Parameters
        ----------
        other_mesh : pyvista.PolyData
            Mesh operating on the source mesh.

        tolerance : float, default: 1e-5
            Tolerance used to determine when a point's absolute
            distance is considered to be zero.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The result of the boolean operation.

        Examples
        --------
        Demonstrate a boolean difference with two spheres.  Note how
        the final mesh only includes ``sphere_a``.

        >>> import pyvista
        >>> sphere_a = pyvista.Sphere()
        >>> sphere_b = pyvista.Sphere(center=(0.5, 0, 0))
        >>> result = sphere_a.boolean_difference(sphere_b)
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     sphere_a, color='r', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(
        ...     sphere_b, color='b', style='wireframe', line_width=3
        ... )
        >>> _ = pl.add_mesh(result, color='tan')
        >>> pl.camera_position = 'xz'
        >>> pl.show()

        See :ref:`boolean_example` for more examples using this filter.

        """
        return self._boolean('difference', other_mesh, tolerance, progress_bar=progress_bar)

    def __add__(self, dataset):
        """Merge these two meshes."""
        return self.merge(dataset)

    def __iadd__(self, dataset):
        """Merge another mesh into this one if possible.

        "If possible" means that ``dataset`` is also a :class:`PolyData`.
        Otherwise we have to return a :class:`pyvista.UnstructuredGrid`,
        so the in-place merge attempt will raise.

        """
        merged = self.merge(dataset, inplace=True)
        return merged

    def merge(
        self,
        dataset,
        merge_points=True,
        tolerance=0.0,
        inplace=False,
        main_has_priority=True,
        progress_bar=False,
    ):
        """Merge this mesh with one or more datasets.

        .. note::
           The behavior of this filter varies from the
           :func:`PolyDataFilters.boolean_union` filter.  This filter
           does not attempt to create a manifold mesh and will include
           internal surfaces when two meshes overlap.

        .. note::
           The ``+`` operator between two meshes uses this filter with
           the default parameters. When the other mesh is also a
           :class:`pyvista.PolyData`, in-place merging via ``+=`` is
           similarly possible.

        Parameters
        ----------
        dataset : pyvista.DataSet
            PyVista dataset to merge this mesh with.

        merge_points : bool, optional
            Merge equivalent points when ``True``.

        tolerance : float, default: 0.0
            The absolute tolerance to use to find coincident points when
            ``merge_points=True``.

        inplace : bool, default: False
            Updates grid inplace when ``True`` if the input type is a
            :class:`pyvista.PolyData`. For other input meshes the
            result is a :class:`pyvista.UnstructuredGrid` which makes
            in-place operation impossible.

        main_has_priority : bool, optional
            When this parameter is ``True`` and ``merge_points=True``,
            the arrays of the merging grids will be overwritten
            by the original main mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.DataSet
            :class:`pyvista.PolyData` if ``dataset`` is a
            :class:`pyvista.PolyData`, otherwise a
            :class:`pyvista.UnstructuredGrid`.

        Examples
        --------
        >>> import pyvista
        >>> sphere_a = pyvista.Sphere()
        >>> sphere_b = pyvista.Sphere(center=(0.5, 0, 0))
        >>> merged = sphere_a.merge(sphere_b)
        >>> merged.plot(style='wireframe', color='tan')

        """
        # check if dataset or datasets are not polydata
        if isinstance(dataset, (list, tuple, pyvista.MultiBlock)):
            is_polydata = all(isinstance(data, pyvista.PolyData) for data in dataset)
        else:
            is_polydata = isinstance(dataset, pyvista.PolyData)

        if inplace and not is_polydata:
            raise TypeError("In-place merge requires both input datasets to be PolyData.")

        merged = DataSetFilters.merge(
            self,
            dataset,
            merge_points=merge_points,
            tolerance=tolerance,
            main_has_priority=main_has_priority,
            inplace=False,
            progress_bar=progress_bar,
        )

        # convert back to a polydata if both inputs were polydata
        if is_polydata:
            # if either of the input datasets contained lines or strips, we
            # must use extract_geometry to ensure they get converted back
            # correctly. This incurrs a performance penalty, but is needed to
            # maintain data consistency.
            if isinstance(dataset, (list, tuple, pyvista.MultiBlock)):
                dataset_has_lines_strips = any(
                    [ds.n_lines or ds.n_strips or ds.n_verts for ds in dataset]
                )
            else:
                dataset_has_lines_strips = dataset.n_lines or dataset.n_strips or dataset.n_verts

            if self.n_lines or self.n_strips or self.n_verts or dataset_has_lines_strips:
                merged = merged.extract_geometry()
            else:
                polydata_merged = pyvista.PolyData(
                    merged.points, faces=merged.cells, n_faces=merged.n_cells, deep=False
                )
                polydata_merged.point_data.update(merged.point_data)
                polydata_merged.cell_data.update(merged.cell_data)
                polydata_merged.field_data.update(merged.field_data)
                merged = polydata_merged

        if inplace:
            self.deep_copy(merged)
            return self

        return merged

    def intersection(self, mesh, split_first=True, split_second=True, progress_bar=False):
        """Compute the intersection between two meshes.

        .. note::
           This method returns the surface intersection from two meshes
           (which often resolves as a line), whereas the
           :func:`PolyDataFilters.boolean_intersection` filter returns
           the "volume" intersection between two closed (manifold)
           meshes.

        Parameters
        ----------
        mesh : pyvista.PolyData
            The mesh to intersect with.

        split_first : bool, default: True
            If ``True``, return the first input mesh split by the
            intersection with the second input mesh.

        split_second : bool, default: True
            If ``True``, return the second input mesh split by the
            intersection with the first input mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The intersection line.

        pyvista.PolyData
            The first mesh split along the intersection. Returns the
            original first mesh if ``split_first=False``.

        pyvista.PolyData
            The second mesh split along the intersection. Returns the
            original second mesh if ``split_second=False``.

        Examples
        --------
        Intersect two spheres, returning the intersection and both spheres
        which have new points/cells along the intersection line.

        >>> import pyvista as pv
        >>> import numpy as np
        >>> s1 = pv.Sphere(phi_resolution=15, theta_resolution=15)
        >>> s2 = s1.copy()
        >>> s2.points += np.array([0.25, 0, 0])
        >>> intersection, s1_split, s2_split = s1.intersection(s2)
        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(s1, style='wireframe')
        >>> _ = pl.add_mesh(s2, style='wireframe')
        >>> _ = pl.add_mesh(intersection, color='r', line_width=10)
        >>> pl.show()

        The mesh splitting takes additional time and can be turned
        off for either mesh individually.

        >>> intersection, _, s2_split = s1.intersection(
        ...     s2, split_first=False, split_second=True
        ... )

        """
        intfilter = _vtk.vtkIntersectionPolyDataFilter()
        intfilter.SetInputDataObject(0, self)
        intfilter.SetInputDataObject(1, mesh)
        intfilter.SetComputeIntersectionPointArray(True)
        intfilter.SetSplitFirstOutput(split_first)
        intfilter.SetSplitSecondOutput(split_second)
        _update_alg(intfilter, progress_bar, 'Computing the intersection between two meshes')

        intersection = _get_output(intfilter, oport=0)
        first = _get_output(intfilter, oport=1)
        second = _get_output(intfilter, oport=2)

        return intersection, first, second

    def curvature(self, curv_type='mean', progress_bar=False):
        """Return the pointwise curvature of a mesh.

        Parameters
        ----------
        curv_type : str, default: "mean"
            Curvature type.  One of the following:

            * ``"mean"``
            * ``"gaussian"``
            * ``"maximum"``
            * ``"minimum"``

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        numpy.ndarray
            Array of curvature values.

        Examples
        --------
        Calculate the mean curvature of the hills example mesh and plot it.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> curv = hills.curvature()
        >>> hills.plot(scalars=curv)

        Show the curvature array.

        >>> curv  # doctest:+SKIP
        array([0.20587616, 0.06747695, ..., 0.11781171, 0.15988467])

        """
        curv_type = curv_type.lower()

        # Create curve filter and compute curvature
        curvefilter = _vtk.vtkCurvatures()
        curvefilter.SetInputData(self)
        if curv_type == 'mean':
            curvefilter.SetCurvatureTypeToMean()
        elif curv_type == 'gaussian':
            curvefilter.SetCurvatureTypeToGaussian()
        elif curv_type == 'maximum':
            curvefilter.SetCurvatureTypeToMaximum()
        elif curv_type == 'minimum':
            curvefilter.SetCurvatureTypeToMinimum()
        else:
            raise ValueError(
                '``curv_type`` must be either "Mean", "Gaussian", "Maximum", or "Minimum".'
            )
        _update_alg(curvefilter, progress_bar, 'Computing Curvature')

        # Compute and return curvature
        curv = _get_output(curvefilter)
        return _vtk.vtk_to_numpy(curv.GetPointData().GetScalars())

    def plot_curvature(self, curv_type='mean', **kwargs):
        """Plot the curvature.

        Parameters
        ----------
        curv_type : str, default: "mean"
            One of the following strings indicating curvature type:

            * ``'mean'``
            * ``'gaussian'``
            * ``'maximum'``
            * ``'minimum'``

        **kwargs : dict, optional
            See :func:`pyvista.plot`.

        Returns
        -------
        pyvista.CameraPosition
            List of camera position, focal point, and view up.
            Returned when ``return_cpos`` is ``True``.

        Examples
        --------
        Plot the Gaussian curvature of an example mesh.  Override the
        default scalar bar range as the mesh edges report high
        curvature.

        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> hills.plot_curvature(
        ...     curv_type='gaussian', smooth_shading=True, clim=[0, 1]
        ... )

        """
        kwargs.setdefault('scalar_bar_args', {'title': f'{curv_type.capitalize()} Curvature'})
        return self.plot(scalars=self.curvature(curv_type), **kwargs)

    def triangulate(self, inplace=False, progress_bar=False):
        """Return an all triangle mesh.

        More complex polygons will be broken down into triangles.

        Parameters
        ----------
        inplace : bool, default: False
            Whether to update the mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing only triangles.

        Examples
        --------
        Generate a mesh with quadrilateral faces.

        >>> import pyvista
        >>> plane = pyvista.Plane()
        >>> plane.point_data.clear()
        >>> plane.plot(show_edges=True, line_width=5)

        Convert it to an all triangle mesh.

        >>> mesh = plane.triangulate()
        >>> mesh.plot(show_edges=True, line_width=5)

        """
        trifilter = _vtk.vtkTriangleFilter()
        trifilter.SetInputData(self)
        trifilter.PassVertsOff()
        trifilter.PassLinesOff()
        _update_alg(trifilter, progress_bar, 'Computing Triangle Mesh')

        mesh = _get_output(trifilter)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def smooth(
        self,
        n_iter=20,
        relaxation_factor=0.01,
        convergence=0.0,
        edge_angle=15,
        feature_angle=45,
        boundary_smoothing=True,
        feature_smoothing=False,
        inplace=False,
        progress_bar=False,
    ):
        """Adjust point coordinates using Laplacian smoothing.

        The effect is to "relax" the mesh, making the cells better shaped and
        the vertices more evenly distributed.

        Parameters
        ----------
        n_iter : int, default: 20
            Number of iterations for Laplacian smoothing.

        relaxation_factor : float, default: 0.01
            Relaxation factor controls the amount of displacement in a single
            iteration. Generally a lower relaxation factor and higher number of
            iterations is numerically more stable.

        convergence : float, default: 0.0
            Convergence criterion for the iteration process. Smaller numbers
            result in more smoothing iterations. Range from (0 to 1).

        edge_angle : float, default: 15
            Edge angle to control smoothing along edges (either interior or boundary).

        feature_angle : float, default: 45
            Feature angle for sharp edge identification.

        boundary_smoothing : bool, default: True
            Flag to control smoothing of boundary edges. When ``True``,
            boundary edges remain fixed.

        feature_smoothing : bool, default: False
            Flag to control smoothing of feature edges.  When ``True``,
            boundary edges remain fixed as defined by ``feature_angle`` and
            ``edge_angle``.

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Smoothed mesh.

        Examples
        --------
        Smooth the edges of an all triangular cube

        >>> import pyvista as pv
        >>> cube = pv.Cube().triangulate().subdivide(5)
        >>> smooth_cube = cube.smooth(1000, feature_smoothing=False)
        >>> n_edge_cells = cube.extract_feature_edges().n_cells
        >>> n_smooth_cells = smooth_cube.extract_feature_edges().n_cells
        >>> f'Sharp Edges on Cube:        {n_edge_cells}'
        'Sharp Edges on Cube:        384'
        >>> f'Sharp Edges on Smooth Cube: {n_smooth_cells}'
        'Sharp Edges on Smooth Cube: 12'
        >>> smooth_cube.plot()

        See :ref:`surface_smoothing_example` for more examples using this filter.

        """
        alg = _vtk.vtkSmoothPolyDataFilter()
        alg.SetInputData(self)
        alg.SetNumberOfIterations(n_iter)
        alg.SetConvergence(convergence)
        alg.SetFeatureEdgeSmoothing(feature_smoothing)
        alg.SetFeatureAngle(feature_angle)
        alg.SetEdgeAngle(edge_angle)
        alg.SetBoundarySmoothing(boundary_smoothing)
        alg.SetRelaxationFactor(relaxation_factor)
        _update_alg(alg, progress_bar, 'Smoothing Mesh')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def smooth_taubin(
        self,
        n_iter=20,
        pass_band=0.1,
        edge_angle=15.0,
        feature_angle=45.0,
        boundary_smoothing=True,
        feature_smoothing=False,
        non_manifold_smoothing=False,
        normalize_coordinates=False,
        inplace=False,
        progress_bar=False,
    ):
        """Smooth a PolyData DataSet with Taubin smoothing.

        This filter allows you to smooth the mesh as in the Laplacian smoothing
        implementation in :func:`smooth() <PolyDataFilters.smooth>`. However,
        unlike Laplacian smoothing the surface does not "shrink" since this
        filter relies on an alternative approach to smoothing. This filter is
        more akin to a low pass filter where undesirable high frequency features
        are removed.

        This PyVista filter uses the VTK `vtkWindowedSincPolyDataFilter
        <https://vtk.org/doc/nightly/html/classvtkWindowedSincPolyDataFilter.html>`_
        filter.

        Parameters
        ----------
        n_iter : int, default: 20
            This is the degree of the polynomial used to approximate the
            windowed sync function. This is generally much less than the number
            needed by :func:`smooth() <PolyDataFilters.smooth>`.

        pass_band : float, default: 0.1
            The passband value for the windowed sinc filter. This should be
            between 0 and 2, where lower values cause more smoothing.

        edge_angle : float, default: 15.0
            Edge angle to control smoothing along edges (either interior or
            boundary).

        feature_angle : float, default: 45.0
            Feature angle for sharp edge identification.

        boundary_smoothing : bool, default: True
            Flag to control smoothing of boundary edges. When ``True``,
            boundary edges remain fixed.

        feature_smoothing : bool, default: False
            Flag to control smoothing of feature edges.  When ``True``,
            boundary edges remain fixed as defined by ``feature_angle`` and
            ``edge_angle``.

        non_manifold_smoothing : bool, default: False
            Smooth non-manifold points.

        normalize_coordinates : bool, default: False
            Flag to control coordinate normalization. To improve the
            numerical stability of the solution and minimize the scaling of the
            translation effects, the algorithm can translate and scale the
            position coordinates to within the unit cube ``[-1, 1]``, perform the
            smoothing, and translate and scale the position coordinates back to
            the original coordinate frame.

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Smoothed mesh.

        Notes
        -----
        For maximum performance, do not enable ``feature_smoothing`` or
        ``boundary_smoothing``. ``feature_smoothing`` is especially expensive.

        References
        ----------
        See `Optimal Surface Smoothing as Filter Design
        <https://dl.acm.org/doi/pdf/10.1145/218380.218473>` for details
        regarding the implementation of Taubin smoothing.

        Examples
        --------
        Smooth the example bone mesh. Here, it's necessary to subdivide the
        mesh to increase the number of faces as the original mesh is so coarse.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> mesh = examples.download_foot_bones().subdivide(2)
        >>> smoothed_mesh = mesh.smooth_taubin()
        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_mesh(mesh)
        >>> _ = pl.add_text('Original Mesh')
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_mesh(smoothed_mesh)
        >>> _ = pl.add_text('Smoothed Mesh')
        >>> pl.show()

        See :ref:`surface_smoothing_example` for more examples using this filter.

        """
        alg = _vtk.vtkWindowedSincPolyDataFilter()
        alg.SetInputData(self)
        alg.SetNumberOfIterations(n_iter)
        alg.SetFeatureEdgeSmoothing(feature_smoothing)
        alg.SetNonManifoldSmoothing(non_manifold_smoothing)
        alg.SetFeatureAngle(feature_angle)
        alg.SetEdgeAngle(edge_angle)
        alg.SetBoundarySmoothing(boundary_smoothing)
        alg.SetPassBand(pass_band)
        alg.SetNormalizeCoordinates(normalize_coordinates)
        _update_alg(alg, progress_bar, 'Smoothing Mesh using Taubin Smoothing')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def decimate_pro(
        self,
        reduction,
        feature_angle=45.0,
        split_angle=75.0,
        splitting=True,
        pre_split_mesh=False,
        preserve_topology=False,
        boundary_vertex_deletion=True,
        max_degree=None,
        inplace=False,
        progress_bar=False,
    ):
        """Reduce the number of triangles in a triangular mesh.

        It forms a good approximation to the original geometry. Based
        on the algorithm originally described in "Decimation of
        Triangle Meshes", Proc Siggraph 92
        (https://doi.org/10.1145/133994.134010).

        Parameters
        ----------
        reduction : float
            Reduction factor. A value of 0.9 will leave 10% of the
            original number of vertices.

        feature_angle : float, default: 45.0
            Angle used to define what an edge is (i.e., if the surface
            normal between two adjacent triangles is >= ``feature_angle``,
            an edge exists).

        split_angle : float, default: 75.0
            Angle used to control the splitting of the mesh. A split
            line exists when the surface normals between two edge
            connected triangles are >= ``split_angle``.

        splitting : bool, default: True
            Controls the splitting of the mesh at corners, along
            edges, at non-manifold points, or anywhere else a split is
            required. Turning splitting off will better preserve the
            original topology of the mesh, but may not necessarily
            give the exact requested decimation.

        pre_split_mesh : bool, default: False
            Separates the mesh into semi-planar patches, which are
            disconnected from each other. This can give superior
            results in some cases. If ``pre_split_mesh`` is set to
            ``True``, the mesh is split with the specified
            ``split_angle``. Otherwise mesh splitting is deferred as
            long as possible.

        preserve_topology : bool, default: False
            Controls topology preservation. If on, mesh splitting and
            hole elimination will not occur. This may limit the
            maximum reduction that may be achieved.

        boundary_vertex_deletion : bool, default: True
            Allow deletion of vertices on the boundary of the mesh.
            Turning this off may limit the maximum reduction that may
            be achieved.

        max_degree : float, optional
            The maximum vertex degree. If the number of triangles
            connected to a vertex exceeds ``max_degree``, then the
            vertex will be split. The complexity of the triangulation
            algorithm is proportional to ``max_degree**2``. Setting ``max_degree``
            small can improve the performance of the algorithm.

        inplace : bool, default: False
            Whether to update the mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Decimated mesh.

        Examples
        --------
        Decimate a sphere.  First plot the sphere.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(phi_resolution=60, theta_resolution=60)
        >>> sphere.plot(show_edges=True, line_width=2)

        Now decimate it and plot it.

        >>> decimated = sphere.decimate_pro(0.75)
        >>> decimated.plot(show_edges=True, line_width=2)

        See :ref:`decimate_example` for more examples using this filter.

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for decimation must be all triangles.")

        alg = _vtk.vtkDecimatePro()
        alg.SetInputData(self)
        alg.SetTargetReduction(reduction)
        alg.SetPreserveTopology(preserve_topology)
        alg.SetFeatureAngle(feature_angle)
        alg.SetSplitting(splitting)
        alg.SetSplitAngle(split_angle)
        alg.SetPreSplitMesh(pre_split_mesh)
        alg.SetBoundaryVertexDeletion(boundary_vertex_deletion)

        if max_degree is not None:
            alg.SetDegree(max_degree)

        _update_alg(alg, progress_bar, 'Decimating Mesh')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self

        return mesh

    def tube(
        self,
        radius=None,
        scalars=None,
        capping=True,
        n_sides=20,
        radius_factor=10.0,
        absolute=False,
        preference='point',
        inplace=False,
        progress_bar=False,
    ):
        """Generate a tube around each input line.

        The radius of the tube can be set to linearly vary with a
        scalar value.

        Parameters
        ----------
        radius : float, optional
            Minimum tube radius (minimum because the tube radius may
            vary).

        scalars : str, optional
            Scalars array by which the radius varies.

        capping : bool, default: True
            Turn on/off whether to cap the ends with polygons.

        n_sides : int, default: 20
            Set the number of sides for the tube. Minimum of 3.

        radius_factor : float, default: 10.0
            Maximum tube radius in terms of a multiple of the minimum
            radius.

        absolute : bool, default: False
            Vary the radius with values from scalars in absolute units.

        preference : str, default: 'point'
            The field preference when searching for the scalars array by
            name.

        inplace : bool, default: False
            Whether to update the mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Tube-filtered mesh.

        Examples
        --------
        Convert a single line to a tube.

        >>> import pyvista as pv
        >>> line = pv.Line()
        >>> tube = line.tube(radius=0.02)
        >>> f'Line Cells: {line.n_cells}'
        'Line Cells: 1'
        >>> f'Tube Cells: {tube.n_cells}'
        'Tube Cells: 22'
        >>> tube.plot(color='tan')

        See :ref:`create_spline_example` for more examples using this filter.

        """
        poly_data = self
        if not isinstance(poly_data, pyvista.PolyData):
            poly_data = pyvista.PolyData(poly_data)
        if n_sides < 3:
            n_sides = 3
        tube = _vtk.vtkTubeFilter()
        tube.SetInputDataObject(poly_data)
        # User Defined Parameters
        tube.SetCapping(capping)
        if radius is not None:
            tube.SetRadius(radius)
        tube.SetNumberOfSides(n_sides)
        tube.SetRadiusFactor(radius_factor)
        # Check if scalars array given
        if scalars is not None:
            if not isinstance(scalars, str):
                raise TypeError('scalars array must be given as a string name')
            field = poly_data.get_array_association(scalars, preference=preference)
            # args: (idx, port, connection, field, name)
            tube.SetInputArrayToProcess(0, 0, 0, field.value, scalars)
            if absolute:
                tube.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
            else:
                tube.SetVaryRadiusToVaryRadiusByScalar()
        # Apply the filter
        _update_alg(tube, progress_bar, 'Creating Tube')

        mesh = _get_output(tube)
        if inplace:
            poly_data.copy_from(mesh, deep=False)
            return poly_data
        return mesh

    def subdivide(self, nsub, subfilter='linear', inplace=False, progress_bar=False):
        """Increase the number of triangles in a single, connected triangular mesh.

        Uses one of the following vtk subdivision filters to subdivide a mesh:

        * ``vtkButterflySubdivisionFilter``
        * ``vtkLoopSubdivisionFilter``
        * ``vtkLinearSubdivisionFilter``

        Linear subdivision results in the fastest mesh subdivision,
        but it does not smooth mesh edges, but rather splits each
        triangle into 4 smaller triangles.

        Butterfly and loop subdivision perform smoothing when
        dividing, and may introduce artifacts into the mesh when
        dividing.

        .. note::
           Subdivision filter sometimes fails for multiple part
           meshes.  The input should be one connected mesh.

        Parameters
        ----------
        nsub : int
            Number of subdivisions.  Each subdivision creates 4 new
            triangles, so the number of resulting triangles is
            ``nface*4**nsub`` where ``nface`` is the current number of
            faces.

        subfilter : str, default: "linear"
            Can be one of the following:

            * ``'butterfly'``
            * ``'loop'``
            * ``'linear'``

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Subdivided mesh.

        Examples
        --------
        First, create an example coarse sphere mesh and plot it.

        >>> from pyvista import examples
        >>> import pyvista
        >>> mesh = pyvista.Sphere(phi_resolution=10, theta_resolution=10)
        >>> mesh.plot(show_edges=True, line_width=3)

        Subdivide the sphere mesh using linear subdivision.

        >>> submesh = mesh.subdivide(1, 'linear')
        >>> submesh.plot(show_edges=True, line_width=3)

        Subdivide the sphere mesh using loop subdivision.

        >>> submesh = mesh.subdivide(1, 'loop')
        >>> submesh.plot(show_edges=True, line_width=3)

        Subdivide the sphere mesh using butterfly subdivision.

        >>> submesh = mesh.subdivide(1, 'butterfly')
        >>> submesh.plot(show_edges=True, line_width=3)

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for subdivision must be all triangles.")

        subfilter = subfilter.lower()
        if subfilter == 'linear':
            sfilter = _vtk.vtkLinearSubdivisionFilter()
        elif subfilter == 'butterfly':
            sfilter = _vtk.vtkButterflySubdivisionFilter()
        elif subfilter == 'loop':
            sfilter = _vtk.vtkLoopSubdivisionFilter()
        else:
            raise ValueError(
                "Subdivision filter must be one of the following: "
                "'butterfly', 'loop', or 'linear'"
            )

        # Subdivide
        sfilter.SetCheckForTriangles(False)  # we already check for this
        sfilter.SetNumberOfSubdivisions(nsub)
        sfilter.SetInputData(self)
        _update_alg(sfilter, progress_bar, 'Subdividing Mesh')

        submesh = _get_output(sfilter)
        if inplace:
            self.copy_from(submesh, deep=False)
            return self

        return submesh

    def subdivide_adaptive(
        self,
        max_edge_len=None,
        max_tri_area=None,
        max_n_tris=None,
        max_n_passes=None,
        inplace=False,
        progress_bar=False,
    ):
        """Increase the number of triangles in a triangular mesh based on edge and/or area metrics.

        This filter uses a simple case-based, multi-pass approach to
        repeatedly subdivide the input triangle mesh to meet the area
        and/or edge length criteria. New points may be inserted only
        on edges; depending on the number of edges to be subdivided a
        different number of triangles are inserted ranging from two
        (i.e., two triangles replace the original one) to four.

        Point and cell data is treated as follows: The cell data from
        a parent triangle is assigned to its subdivided
        children. Point data is interpolated along edges as the edges
        are subdivided.

        This filter retains mesh watertightness if the mesh was
        originally watertight; and the area and max triangles criteria
        are not used.

        Parameters
        ----------
        max_edge_len : float, optional
            The maximum edge length that a triangle may have. Edges
            longer than this value are split in half and the
            associated triangles are modified accordingly.

        max_tri_area : float, optional
            The maximum area that a triangle may have. Triangles
            larger than this value are subdivided to meet this
            threshold. Note that if this criterion is used it may
            produce non-watertight meshes as a result.

        max_n_tris : int, optional
            The maximum number of triangles that can be created. If
            the limit is hit, it may result in premature termination
            of the algorithm and the results may be less than
            satisfactory (for example non-watertight meshes may be
            created). By default, the limit is set to a very large
            number (i.e., no effective limit).

        max_n_passes : int, optional
            The maximum number of passes (i.e., levels of
            subdivision). If the limit is hit, then the subdivision
            process stops and additional passes (needed to meet other
            criteria) are aborted. The default limit is set to a very
            large number (i.e., no effective limit).

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Subdivided mesh.

        Examples
        --------
        First, load the example airplane mesh and plot it.

        >>> import pyvista
        >>> from pyvista import examples
        >>> mesh = pyvista.PolyData(examples.planefile)
        >>> mesh.plot(show_edges=True, line_width=3)

        Subdivide the mesh

        >>> submesh = mesh.subdivide_adaptive(max_n_passes=2)
        >>> submesh.plot(show_edges=True)

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for subdivision must be all triangles.")

        sfilter = _vtk.vtkAdaptiveSubdivisionFilter()
        if max_edge_len:
            sfilter.SetMaximumEdgeLength(max_edge_len)
        if max_tri_area:
            sfilter.SetMaximumTriangleArea(max_tri_area)
        if max_n_tris:
            sfilter.SetMaximumNumberOfTriangles(max_n_tris)
        if max_n_passes:
            sfilter.SetMaximumNumberOfPasses(max_n_passes)

        sfilter.SetInputData(self)
        _update_alg(sfilter, progress_bar, 'Adaptively Subdividing Mesh')
        submesh = _get_output(sfilter)

        if inplace:
            self.copy_from(submesh, deep=False)
            return self

        return submesh

    def decimate(
        self,
        target_reduction,
        volume_preservation=False,
        attribute_error=False,
        scalars=True,
        vectors=True,
        normals=False,
        tcoords=True,
        tensors=True,
        scalars_weight=0.1,
        vectors_weight=0.1,
        normals_weight=0.1,
        tcoords_weight=0.1,
        tensors_weight=0.1,
        inplace=False,
        progress_bar=False,
    ):
        """Reduce the number of triangles in a triangular mesh using ``vtkQuadricDecimation``.

        Parameters
        ----------
        target_reduction : float
            Fraction of the original mesh to remove.
            If ``target_reduction`` is set to 0.9, this filter will try
            to reduce the data set to 10% of its original size and will
            remove 90% of the input triangles.

        volume_preservation : bool, default: False
            Decide whether to activate volume preservation which greatly
            reduces errors in triangle normal direction. If ``False``,
            volume preservation is disabled and if ``attribute_error``
            is active, these errors can be large.

        attribute_error : bool, default: False
            Decide whether to include data attributes in the error metric. If
            ``False``, then only geometric error is used to control the
            decimation. If ``True``, the following flags are used to specify
            which attributes are to be included in the error calculation.

        scalars : bool, default: True
            If attribute errors are to be included in the metric (i.e.,
            ``attribute_error`` is ``True``), then these flags control
            which attributes are to be included in the error
            calculation.

        vectors : bool, default: True
            See ``scalars`` parameter.

        normals : bool, default: False
            See ``scalars`` parameter.

        tcoords : bool, default: True
            See ``scalars`` parameter.

        tensors : bool, default: True
            See ``scalars`` parameter.

        scalars_weight : float, default: 0.1
            The scaling weight contribution of the scalar attribute.
            These values are used to weight the contribution of the
            attributes towards the error metric.

        vectors_weight : float, default: 0.1
            See ``scalars_weight`` parameter.

        normals_weight : float, default: 0.1
            See ``scalars_weight`` parameter.

        tcoords_weight : float, default: 0.1
            See ``scalars_weight`` parameter.

        tensors_weight : float, default: 0.1
            See ``scalars_weight`` parameter.

        inplace : bool, default: False
            Whether to update the mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Decimated mesh.

        Notes
        -----
        If you encounter a segmentation fault or other error, consider using
        :func:`pyvista.PolyDataFilters.clean` to remove any invalid cells
        before using this filter.

        Examples
        --------
        Decimate a sphere.  First plot the sphere.

        >>> import pyvista
        >>> sphere = pyvista.Sphere(phi_resolution=60, theta_resolution=60)
        >>> sphere.plot(show_edges=True, line_width=2)

        Now decimate it by 75% and plot it.

        >>> decimated = sphere.decimate(0.75)
        >>> decimated.plot(show_edges=True, line_width=2)

        See :ref:`decimate_example` for more examples using this filter.

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for decimation must be all triangles.")

        # create decimation filter
        alg = _vtk.vtkQuadricDecimation()

        alg.SetVolumePreservation(volume_preservation)
        alg.SetAttributeErrorMetric(attribute_error)
        alg.SetScalarsAttribute(scalars)
        alg.SetVectorsAttribute(vectors)
        alg.SetNormalsAttribute(normals)
        alg.SetTCoordsAttribute(tcoords)
        alg.SetTensorsAttribute(tensors)
        alg.SetScalarsWeight(scalars_weight)
        alg.SetVectorsWeight(vectors_weight)
        alg.SetNormalsWeight(normals_weight)
        alg.SetTCoordsWeight(tcoords_weight)
        alg.SetTensorsWeight(tensors_weight)
        alg.SetTargetReduction(target_reduction)

        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Decimating Mesh')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self

        return mesh

    def compute_normals(
        self,
        cell_normals=True,
        point_normals=True,
        split_vertices=False,
        flip_normals=False,
        consistent_normals=True,
        auto_orient_normals=False,
        non_manifold_traversal=True,
        feature_angle=30.0,
        inplace=False,
        progress_bar=False,
    ):
        """Compute point and/or cell normals for a mesh.

        The filter can reorder polygons to insure consistent
        orientation across polygon neighbors. Sharp edges can be split
        and points duplicated with separate normals to give crisp
        (rendered) surface definition. It is also possible to globally
        flip the normal orientation.

        The algorithm works by determining normals for each polygon
        and then averaging them at shared points. When sharp edges are
        present, the edges are split and new points generated to
        prevent blurry edges (due to Phong shading).

        Parameters
        ----------
        cell_normals : bool, default: True
            Calculation of cell normals.

        point_normals : bool, default: True
            Calculation of point normals.

        split_vertices : bool, default: False
            Splitting of sharp edges. Indices to the original points are
            tracked in the ``"pyvistaOriginalPointIds"`` array.

        flip_normals : bool, default: False
            Set global flipping of normal orientation. Flipping
            modifies both the normal direction and the order of a
            cell's points.

        consistent_normals : bool, default: True
            Enforcement of consistent polygon ordering.

        auto_orient_normals : bool, default: False
            Turn on/off the automatic determination of correct normal
            orientation. NOTE: This assumes a completely closed
            surface (i.e. no boundary edges) and no non-manifold
            edges. If these constraints do not hold, all bets are
            off. This option adds some computational complexity, and
            is useful if you do not want to have to inspect the
            rendered image to determine whether to turn on the
            ``flip_normals`` flag.  However, this flag can work with
            the ``flip_normals`` flag, and if both are set, all the
            normals in the output will point "inward".

        non_manifold_traversal : bool, default: True
            Turn on/off traversal across non-manifold edges. Changing
            this may prevent problems where the consistency of
            polygonal ordering is corrupted due to topological
            loops.

        feature_angle : float, default: 30.0
            The angle that defines a sharp edge. If the difference in
            angle across neighboring polygons is greater than this
            value, the shared edge is considered "sharp".

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Updated mesh with cell and point normals.

        Notes
        -----
        Previous arrays named ``"Normals"`` will be overwritten.

        Normals are computed only for polygons and triangle
        strips. Normals are not computed for lines or vertices.

        Triangle strips are broken up into triangle polygons. You may
        want to restrip the triangles.

        It may be easier to run
        :func:`pyvista.PolyData.point_normals` or
        :func:`pyvista.PolyData.cell_normals` if you would just
        like the array of point or cell normals.

        Examples
        --------
        Compute the point normals of the surface of a sphere.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> sphere = sphere.compute_normals(cell_normals=False)
        >>> normals = sphere['Normals']
        >>> normals.shape
        (842, 3)

        Alternatively, create a new mesh when computing the normals
        and compute both cell and point normals.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> sphere_with_norm = sphere.compute_normals()
        >>> sphere_with_norm.point_data['Normals'].shape
        (842, 3)
        >>> sphere_with_norm.cell_data['Normals'].shape
        (1680, 3)

        See :ref:`surface_normal_example` for more examples using this filter.

        """
        # track original point indices
        if split_vertices:
            self.point_data.set_array(
                np.arange(self.n_points, dtype=pyvista.ID_TYPE), 'pyvistaOriginalPointIds'
            )

        normal = _vtk.vtkPolyDataNormals()
        normal.SetComputeCellNormals(cell_normals)
        normal.SetComputePointNormals(point_normals)
        normal.SetSplitting(split_vertices)
        normal.SetFlipNormals(flip_normals)
        normal.SetConsistency(consistent_normals)
        normal.SetAutoOrientNormals(auto_orient_normals)
        normal.SetNonManifoldTraversal(non_manifold_traversal)
        normal.SetFeatureAngle(feature_angle)
        normal.SetInputData(self)
        _update_alg(normal, progress_bar, 'Computing Normals')

        mesh = _get_output(normal)
        if point_normals:
            mesh.GetPointData().SetActiveNormals('Normals')
        if cell_normals:
            mesh.GetCellData().SetActiveNormals('Normals')

        if inplace:
            self.copy_from(mesh, deep=False)
            return self

        return mesh

    def clip_closed_surface(
        self, normal='x', origin=None, tolerance=1e-06, inplace=False, progress_bar=False
    ):
        """Clip a closed polydata surface with a plane.

        This currently only supports one plane but could be
        implemented to handle a plane collection.

        It will produce a new closed surface by creating new polygonal
        faces where the input data was clipped.

        Non-manifold surfaces should not be used as input for this
        filter.  The input surface should have no open edges, and must
        not have any edges that are shared by more than two faces. In
        addition, the input surface should not self-intersect, meaning
        that the faces of the surface should only touch at their
        edges.

        Parameters
        ----------
        normal : str, list, optional
            Plane normal to clip with.  Plane is centered at
            ``origin``.  Normal can be either a 3 member list
            (e.g. ``[0, 0, 1]``) or one of the following strings:
            ``'x'``, ``'y'``, ``'z'``, ``'-x'``, ``'-y'``, or
            ``'-z'``.

        origin : list, optional
            Coordinate of the origin (e.g. ``[1, 0, 0]``).  Defaults
            to the center of the mesh.

        tolerance : float, optional
            The tolerance for creating new points while clipping.  If
            the tolerance is too small, then degenerate triangles
            might be produced.

        inplace : bool, default: False
            Updates mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            The clipped mesh.

        Examples
        --------
        Clip a sphere in the X direction centered at the origin.  This
        will leave behind half a sphere in the positive X direction.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> clipped_mesh = sphere.clip_closed_surface('-z')
        >>> clipped_mesh.plot(show_edges=True, line_width=3)

        Clip the sphere at the XY plane and leave behind half the
        sphere in the positive Z direction.  Shift the clip upwards to
        leave a smaller mesh behind.

        >>> clipped_mesh = sphere.clip_closed_surface(
        ...     'z', origin=[0, 0, 0.3]
        ... )
        >>> clipped_mesh.plot(show_edges=True, line_width=3)

        """
        # verify it is manifold
        if self.n_open_edges > 0:
            raise ValueError("This surface appears to be non-manifold.")
        if isinstance(normal, str):
            normal = NORMALS[normal.lower()]
        # find center of data if origin not specified
        if origin is None:
            origin = self.center

        # create the plane for clipping
        plane = generate_plane(normal, origin)
        collection = _vtk.vtkPlaneCollection()
        collection.AddItem(plane)

        alg = _vtk.vtkClipClosedSurface()
        alg.SetGenerateFaces(True)
        alg.SetInputDataObject(self)
        alg.SetTolerance(tolerance)
        alg.SetClippingPlanes(collection)
        _update_alg(alg, progress_bar, 'Clipping Closed Surface')
        result = _get_output(alg)

        if inplace:
            self.copy_from(result, deep=False)
            return self
        else:
            return result

    def fill_holes(self, hole_size, inplace=False, progress_bar=False):  # pragma: no cover
        """Fill holes in a pyvista.PolyData or vtk.vtkPolyData object.

        Holes are identified by locating boundary edges, linking them
        together into loops, and then triangulating the resulting
        loops. Note that you can specify an approximate limit to the
        size of the hole that can be filled.

        .. warning::
           This method is known to segfault.  Use at your own risk.

        Parameters
        ----------
        hole_size : float
            Specifies the maximum hole size to fill. This is
            represented as a radius to the bounding circumsphere
            containing the hole. Note that this is an approximate
            area; the actual area cannot be computed without first
            triangulating the hole.

        inplace : bool, default: False
            Return new mesh or overwrite input.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh with holes filled if ``inplace=False``.

        Examples
        --------
        Create a partial sphere with a hole and then fill it.

        >>> import pyvista as pv
        >>> sphere_with_hole = pv.Sphere(end_theta=330)
        >>> sphere = sphere_with_hole.fill_holes(1000)  # doctest:+SKIP
        >>> edges = sphere.extract_feature_edges(
        ...     feature_edges=False, manifold_edges=False
        ... )  # doctest:+SKIP
        >>> assert edges.n_cells == 0  # doctest:+SKIP

        """
        alg = _vtk.vtkFillHolesFilter()
        alg.SetHoleSize(hole_size)
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Filling Holes')

        mesh = _get_output(alg)
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def clean(
        self,
        point_merging=True,
        tolerance=None,
        lines_to_points=True,
        polys_to_lines=True,
        strips_to_polys=True,
        inplace=False,
        absolute=True,
        progress_bar=False,
        **kwargs,
    ):
        """Clean the mesh.

        This merges duplicate points, removes unused points, and/or
        removes degenerate cells.

        Parameters
        ----------
        point_merging : bool, optional
            Enables point merging.  ``True`` by default.

        tolerance : float, optional
            Set merging tolerance.  When enabled merging is set to
            absolute distance. If ``absolute`` is ``False``, then the
            merging tolerance is a fraction of the bounding box
            length. The alias ``merge_tol`` is also excepted.

        lines_to_points : bool, optional
            Enable or disable the conversion of degenerate lines to
            points.  Enabled by default.

        polys_to_lines : bool, optional
            Enable or disable the conversion of degenerate polys to
            lines.  Enabled by default.

        strips_to_polys : bool, optional
            Enable or disable the conversion of degenerate strips to
            polys.

        inplace : bool, default: False
            Updates mesh in-place.

        absolute : bool, optional
            Control if ``tolerance`` is an absolute distance or a
            fraction.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            Accepts for ``merge_tol`` to replace the ``tolerance``
            keyword argument.  This may be deprecated in future.

        Returns
        -------
        pyvista.PolyData
            Cleaned mesh.

        Examples
        --------
        Create a mesh with a degenerate face and then clean it,
        removing the degenerate face

        >>> import pyvista as pv
        >>> import numpy as np
        >>> points = np.array(
        ...     [[0, 0, 0], [0, 1, 0], [1, 0, 0]], dtype=np.float32
        ... )
        >>> faces = np.array([3, 0, 1, 2, 3, 0, 2, 2])
        >>> mesh = pv.PolyData(points, faces)
        >>> mout = mesh.clean()
        >>> mout.faces  # doctest:+SKIP
        array([3, 0, 1, 2])

        """
        if tolerance is None:
            tolerance = kwargs.pop('merge_tol', None)
        assert_empty_kwargs(**kwargs)
        alg = _vtk.vtkCleanPolyData()
        alg.SetPointMerging(point_merging)
        alg.SetConvertLinesToPoints(lines_to_points)
        alg.SetConvertPolysToLines(polys_to_lines)
        alg.SetConvertStripsToPolys(strips_to_polys)
        if isinstance(tolerance, (int, float)):
            if absolute:
                alg.ToleranceIsAbsoluteOn()
                alg.SetAbsoluteTolerance(tolerance)
            else:
                alg.SetTolerance(tolerance)
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Cleaning')
        output = _get_output(alg)

        # Check output so no segfaults occur
        if output.n_points < 1:
            raise ValueError('Clean tolerance is too high. Empty mesh returned.')

        if inplace:
            self.copy_from(output, deep=False)
            return self
        return output

    def geodesic(
        self,
        start_vertex,
        end_vertex,
        inplace=False,
        keep_order=True,
        use_scalar_weights=False,
        progress_bar=False,
    ):
        """Calculate the geodesic path between two vertices using Dijkstra's algorithm.

        This will add an array titled ``'vtkOriginalPointIds'`` of the input
        mesh's point ids to the output mesh. The default behavior of the
        underlying ``vtkDijkstraGraphGeodesicPath`` filter is that the
        geodesic path is reversed in the resulting mesh. This is overridden
        in PyVista by default.

        Parameters
        ----------
        start_vertex : int
            Vertex index indicating the start point of the geodesic segment.

        end_vertex : int
            Vertex index indicating the end point of the geodesic segment.

        inplace : bool, default: False
            Whether the input mesh should be replaced with the path. The
            geodesic path is always returned.

        keep_order : bool, default: True
            If ``True``, the points of the returned path are guaranteed
            to start with the start vertex (as opposed to the end vertex).

            .. versionadded:: 0.32.0

        use_scalar_weights : bool, default: False
            If ``True``, use scalar values in the edge weight.
            This only works for point data.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            ``PolyData`` object consisting of the line segment between the
            two given vertices. If ``inplace`` is ``True`` this is the
            same object as the input mesh.

        Examples
        --------
        Plot the path between two points on the random hills mesh.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> path = hills.geodesic(560, 5820)
        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(hills)
        >>> _ = pl.add_mesh(path, line_width=5, color='k')
        >>> pl.show()

        See :ref:`geodesic_example` for more examples using this filter.

        """
        if not (0 <= start_vertex < self.n_points and 0 <= end_vertex < self.n_points):
            raise IndexError('Invalid point indices.')
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for geodesic path must be all triangles.")

        dijkstra = _vtk.vtkDijkstraGraphGeodesicPath()
        dijkstra.SetInputData(self)
        dijkstra.SetStartVertex(start_vertex)
        dijkstra.SetEndVertex(end_vertex)
        dijkstra.SetUseScalarWeights(use_scalar_weights)
        _update_alg(dijkstra, progress_bar, 'Calculating the Geodesic Path')
        original_ids = vtk_id_list_to_array(dijkstra.GetIdList())

        output = _get_output(dijkstra)
        if output.n_points == 0:
            raise ValueError(
                f"There is no path between vertices {start_vertex} and {end_vertex}. ",
                "It is likely the vertices belong to disconnected regions.",
            )

        output["vtkOriginalPointIds"] = original_ids

        # Do not copy textures from input
        output.clear_textures()

        # ensure proper order if requested
        if keep_order and original_ids[0] == end_vertex:
            output.points[...] = output.points[::-1, :]
            output["vtkOriginalPointIds"] = output["vtkOriginalPointIds"][::-1]

        if inplace:
            self.copy_from(output, deep=False)
            return self

        return output

    def geodesic_distance(
        self, start_vertex, end_vertex, use_scalar_weights=False, progress_bar=False
    ):
        """Calculate the geodesic distance between two vertices using Dijkstra's algorithm.

        Parameters
        ----------
        start_vertex : int
            Vertex index indicating the start point of the geodesic segment.

        end_vertex : int
            Vertex index indicating the end point of the geodesic segment.

        use_scalar_weights : bool, default: False
            If ``True``, use scalar values in the edge weight.
            This only works for point data.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        float
            Length of the geodesic segment.

        Examples
        --------
        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> length = sphere.geodesic_distance(0, 100)
        >>> f'Length is {length:.3f}'
        'Length is 0.812'

        See :ref:`geodesic_example` for more examples using this filter.

        """
        path = self.geodesic(start_vertex, end_vertex, use_scalar_weights=use_scalar_weights)
        sizes = path.compute_cell_sizes(
            length=True, area=False, volume=False, progress_bar=progress_bar
        )
        distance = np.sum(sizes['Length'])
        del path
        del sizes
        return distance

    def ray_trace(self, origin, end_point, first_point=False, plot=False, off_screen=None):
        """Perform a single ray trace calculation.

        This requires a mesh and a line segment defined by an origin
        and end_point.

        Parameters
        ----------
        origin : sequence[float]
            Start of the line segment.

        end_point : sequence[float]
            End of the line segment.

        first_point : bool, default: False
            Returns intersection of first point only.

        plot : bool, default: False
            Whether to plot the ray trace results.

        off_screen : bool, optional
            Plots off screen when ``plot=True``.  Used for unit testing.

        Returns
        -------
        intersection_points : numpy.ndarray
            Location of the intersection points.  Empty array if no
            intersections.

        intersection_cells : numpy.ndarray
            Indices of the intersection cells.  Empty array if no
            intersections.

        Examples
        --------
        Compute the intersection between a ray from the origin to
        ``[1, 0, 0]`` and a sphere with radius 0.5 centered at the
        origin.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> point, cell = sphere.ray_trace(
        ...     [0, 0, 0], [1, 0, 0], first_point=True
        ... )
        >>> f'Intersected at {point[0]:.3f} {point[1]:.3f} {point[2]:.3f}'
        'Intersected at 0.499 0.000 0.000'

        Show a plot of the ray trace.

        >>> point, cell = sphere.ray_trace([0, 0, 0], [1, 0, 0], plot=True)

        See :ref:`ray_trace_example` for more examples using this filter.

        """
        points = _vtk.vtkPoints()
        cell_ids = _vtk.vtkIdList()
        self.obbTree.IntersectWithLine(np.array(origin), np.array(end_point), points, cell_ids)

        intersection_points = _vtk.vtk_to_numpy(points.GetData())
        if first_point and intersection_points.shape[0] >= 1:
            intersection_points = intersection_points[0]

        intersection_cells = []
        if intersection_points.any():
            if first_point:
                ncells = 1
            else:
                ncells = cell_ids.GetNumberOfIds()
            for i in range(ncells):
                intersection_cells.append(cell_ids.GetId(i))
        intersection_cells = np.array(intersection_cells)

        if plot:
            plotter = pyvista.Plotter(off_screen=off_screen)
            plotter.add_mesh(self, label='Test Mesh')
            segment = np.array([origin, end_point])
            plotter.add_lines(segment, 'b', label='Ray Segment')
            plotter.add_mesh(intersection_points, 'r', point_size=10, label='Intersection Points')
            plotter.add_legend()
            plotter.add_axes()
            plotter.show()

        return intersection_points, intersection_cells

    def multi_ray_trace(
        self, origins, directions, first_point=False, retry=False
    ):  # pragma: no cover
        """Perform multiple ray trace calculations.

        This requires a mesh with only triangular faces, an array of
        origin points and an equal sized array of direction vectors to
        trace along.

        The embree library used for vectorization of the ray traces is
        known to occasionally return no intersections where the VTK
        implementation would return an intersection.  If the result
        appears to be missing some intersection points, set
        ``retry=True`` to run a second pass over rays that returned no
        intersections, using :func:`PolyDataFilters.ray_trace`.

        Parameters
        ----------
        origins : array_like[float]
            Starting point for each trace.

        directions : array_like[float]
            Direction vector for each trace.

        first_point : bool, default: False
            Returns intersection of first point only.

        retry : bool, default: False
            Will retry rays that return no intersections using
            :func:`PolyDataFilters.ray_trace`.

        Returns
        -------
        intersection_points : numpy.ndarray
            Location of the intersection points.  Empty array if no
            intersections.

        intersection_rays : numpy.ndarray
            Indices of the ray for each intersection point. Empty array if no
            intersections.

        intersection_cells : numpy.ndarray
            Indices of the intersection cells.  Empty array if no
            intersections.

        Examples
        --------
        Compute the intersection between rays from the origin in
        directions ``[1, 0, 0]``, ``[0, 1, 0]`` and ``[0, 0, 1]``, and
        a sphere with radius 0.5 centered at the origin

        >>> import pyvista as pv  # doctest:+SKIP
        >>> sphere = pv.Sphere()  # doctest:+SKIP
        >>> points, rays, cells = sphere.multi_ray_trace(
        ...     [[0, 0, 0]] * 3,
        ...     [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
        ...     first_point=True,
        ... )  # doctest:+SKIP
        >>> string = ", ".join(
        ...     [
        ...         f"({point[0]:.3f}, {point[1]:.3f}, {point[2]:.3f})"
        ...         for point in points
        ...     ]
        ... )  # doctest:+SKIP
        >>> f'Rays intersected at {string}'  # doctest:+SKIP
        'Rays intersected at (0.499, 0.000, 0.000), (0.000, 0.497, 0.000), (0.000, 0.000, 0.500)'

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError("Input mesh for multi_ray_trace must be all triangles.")

        try:
            import pyembree  # noqa
            import rtree  # noqa
            import trimesh  # noqa
        except ImportError:
            raise ImportError(
                "To use multi_ray_trace please install trimesh, rtree and pyembree with:\n"
                "\tconda install trimesh rtree pyembree"
            )

        origins = np.asarray(origins)
        directions = np.asarray(directions)
        faces_as_array = self.faces.reshape((self.n_faces, 4))[:, 1:]
        tmesh = trimesh.Trimesh(self.points, faces_as_array)
        locations, index_ray, index_tri = tmesh.ray.intersects_location(
            origins, directions, multiple_hits=not first_point
        )
        if retry:
            # gather intersecting rays in lists
            loc_lst = locations.tolist()
            ray_lst = index_ray.tolist()
            tri_lst = index_tri.tolist()

            # find indices that trimesh failed on
            all_ray_indices = np.arange(len(origins))
            retry_ray_indices = np.setdiff1d(all_ray_indices, index_ray, assume_unique=True)

            # compute ray points for all failed rays at once
            origins_retry = origins[retry_ray_indices, :]  # shape (n_retry, 3)
            directions_retry = directions[retry_ray_indices, :]
            unit_directions = directions_retry / np.linalg.norm(
                directions_retry, axis=1, keepdims=True
            )
            second_points = origins_retry + unit_directions * self.length  # shape (n_retry, 3)

            for id_r, origin, second_point in zip(retry_ray_indices, origins_retry, second_points):
                locs, indices = self.ray_trace(origin, second_point, first_point=first_point)
                if locs.any():
                    if first_point:
                        locs = locs.reshape([1, 3])
                    ray_lst.extend([id_r] * indices.size)
                    tri_lst.extend(indices)
                    loc_lst.extend(locs)

            # sort result arrays by ray index
            index_ray = np.array(ray_lst)
            sorting_inds = index_ray.argsort()
            index_ray = index_ray[sorting_inds]
            index_tri = np.array(tri_lst)[sorting_inds]
            locations = np.array(loc_lst)[sorting_inds]

        return locations, index_ray, index_tri

    def plot_boundaries(self, edge_color="red", line_width=None, progress_bar=False, **kwargs):
        """Plot boundaries of a mesh.

        Parameters
        ----------
        edge_color : ColorLike, default: "red"
            The color of the edges when they are added to the plotter.

        line_width : int, optional
            Width of the boundary lines.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            All additional keyword arguments will be passed to
            :func:`pyvista.Plotter.add_mesh`.

        Returns
        -------
        pyvista.CameraPosition
            List of camera position, focal point, and view up.
            Returned when ``return_cpos`` is ``True``.

        Examples
        --------
        >>> from pyvista import examples
        >>> hills = examples.load_random_hills()
        >>> hills.plot_boundaries(line_width=10)

        """
        edges = DataSetFilters.extract_feature_edges(self, progress_bar=progress_bar)

        plotter = pyvista.Plotter(
            off_screen=kwargs.pop('off_screen', None), notebook=kwargs.pop('notebook', None)
        )
        plotter.add_mesh(
            edges, color=edge_color, style='wireframe', label='Edges', line_width=line_width
        )
        plotter.add_mesh(self, label='Mesh', **kwargs)
        plotter.add_legend()
        return plotter.show()

    def plot_normals(
        self, show_mesh=True, mag=1.0, flip=False, use_every=1, faces=False, color=None, **kwargs
    ):
        """Plot the point normals of a mesh.

        Parameters
        ----------
        show_mesh : bool, default: true
            Plot the mesh itself.

        mag : float, default: 1.0
            Size magnitude of the normal arrows.

        flip : bool, default: False
            Flip the normal direction when ``True``.

        use_every : int, default: 1
            Display every nth normal.  By default every normal is
            displayed.  Display every 10th normal by setting this
            parameter to 10.

        faces : bool, default: False
            Plot face normals instead of the default point normals.

        color : ColorLike, optional
            Color of the arrows.  Defaults to
            :attr:`pyvista.themes.DefaultTheme.edge_color`.

        **kwargs : dict, optional
            All additional keyword arguments will be passed to
            :func:`pyvista.Plotter.add_mesh`.

        Returns
        -------
        pyvista.CameraPosition
            List of camera position, focal point, and view up.
            Returned when ``return_cpos`` is ``True``.

        Examples
        --------
        Plot the point normals of a sphere.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere(phi_resolution=10, theta_resolution=10)
        >>> sphere.plot_normals(mag=0.1, show_edges=True)

        Plot the face normals of a sphere.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere(phi_resolution=10, theta_resolution=10)
        >>> sphere.plot_normals(mag=0.1, faces=True, show_edges=True)

        """
        plotter = pyvista.Plotter(
            off_screen=kwargs.pop('off_screen', None), notebook=kwargs.pop('notebook', None)
        )
        if show_mesh:
            plotter.add_mesh(self, **kwargs)

        color = pyvista.Color(color, default_color=pyvista.global_theme.edge_color)

        if faces:
            centers = self.cell_centers().points[::use_every]
            normals = self.cell_normals
        else:
            centers = self.points[::use_every]
            normals = self.point_normals

        if flip:
            normals *= -1

        plotter.add_arrows(
            centers, normals[::use_every], mag=mag, color=color, show_scalar_bar=False
        )

        return plotter.show()

    def remove_points(self, remove, mode='any', keep_scalars=True, inplace=False):
        """Rebuild a mesh by removing points.

        Only valid for all-triangle meshes.

        Parameters
        ----------
        remove : sequence[bool | int]
            If remove is a bool array, points that are ``True`` will
            be removed.  Otherwise, it is treated as a list of
            indices.

        mode : str, default: "any"
            When ``'all'``, only faces containing all points flagged
            for removal will be removed.

        keep_scalars : bool, default: True
            When ``True``, point and cell scalars will be passed on to
            the new mesh.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.PolyData
            Mesh without the points flagged for removal.

        numpy.ndarray
            Indices of new points relative to the original mesh.

        Examples
        --------
        Remove the first 100 points from a sphere.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> reduced_sphere, ridx = sphere.remove_points(range(100, 250))
        >>> reduced_sphere.plot(show_edges=True, line_width=3)

        """
        remove = np.asarray(remove)

        # np.asarray will eat anything, so we have to weed out bogus inputs
        if not issubclass(remove.dtype.type, (np.bool_, np.integer)):
            raise TypeError('Remove must be either a mask or an integer array-like')

        if remove.dtype == np.bool_:
            if remove.size != self.n_points:
                raise ValueError('Mask different size than n_points')
            remove_mask = remove
        else:
            remove_mask = np.zeros(self.n_points, np.bool_)
            remove_mask[remove] = True

        if not self.is_all_triangles:
            raise NotAllTrianglesError

        f = self.faces.reshape(-1, 4)[:, 1:]
        vmask = remove_mask.take(f)
        if mode == 'all':
            fmask = ~(vmask).all(1)
        else:
            fmask = ~(vmask).any(1)

        # Regenerate face and point arrays
        uni = np.unique(f.compress(fmask, 0), return_inverse=True)
        new_points = self.points.take(uni[0], 0)

        nfaces = fmask.sum()
        faces = np.empty((nfaces, 4), dtype=pyvista.ID_TYPE)
        faces[:, 0] = 3
        faces[:, 1:] = np.reshape(uni[1], (nfaces, 3))

        newmesh = pyvista.PolyData(new_points, faces, deep=True)
        ridx = uni[0]

        # Add scalars back to mesh if requested
        if keep_scalars:
            for key in self.point_data:
                newmesh.point_data[key] = self.point_data[key][ridx]

            for key in self.cell_data:
                try:
                    newmesh.cell_data[key] = self.cell_data[key][fmask]
                except:
                    warnings.warn(f'Unable to pass cell key {key} onto reduced mesh')

        # Return vtk surface and reverse indexing array
        if inplace:
            self.copy_from(newmesh, deep=False)
            return self, ridx
        return newmesh, ridx

    def flip_normals(self):
        """Flip normals of a triangular mesh by reversing the point ordering.

        Examples
        --------
        Flip the normals of a sphere and plot the normals before and
        after the flip.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> sphere.plot_normals(mag=0.1)
        >>> sphere.flip_normals()
        >>> sphere.plot_normals(mag=0.1, opacity=0.5)

        """
        if not self.is_all_triangles:
            raise NotAllTrianglesError('Can only flip normals on an all triangle mesh.')

        f = self._connectivity_array

        # swap first and last point index in-place
        # See: https://stackoverflow.com/a/33362288/
        f[::3], f[2::3] = f[2::3], f[::3].copy()

    def delaunay_2d(
        self,
        tol=1e-05,
        alpha=0.0,
        offset=1.0,
        bound=False,
        inplace=False,
        edge_source=None,
        progress_bar=False,
    ):
        """Apply a 2D Delaunay filter along the best fitting plane.

        This filter can be used to generate a 2d surface from a set of
        points on a plane.  If you want to create a surface from a
        point cloud, see :func:`pyvista.PolyDataFilters.reconstruct_surface`.

        Parameters
        ----------
        tol : float, default: 1e-05
            Specify a tolerance to control discarding of closely
            spaced points. This tolerance is specified as a fraction
            of the diagonal length of the bounding box of the points.

        alpha : float, default: 0.0
            Specify alpha (or distance) value to control output of
            this filter. For a non-zero alpha value, only edges or
            triangles contained within a sphere centered at mesh
            vertices will be output. Otherwise, only triangles will be
            output.

        offset : float, default: 1.0
            Specify a multiplier to control the size of the initial,
            bounding Delaunay triangulation.

        bound : bool, default: False
            Boolean controls whether bounding triangulation points
            and associated triangles are included in the
            output. These are introduced as an initial triangulation
            to begin the triangulation process. This feature is nice
            for debugging output.

        inplace : bool, default: False
            If ``True``, overwrite this mesh with the triangulated
            mesh.

        edge_source : pyvista.PolyData, optional
            Specify the source object used to specify constrained
            edges and loops. If set, and lines/polygons are defined, a
            constrained triangulation is created. The lines/polygons
            are assumed to reference points in the input point set
            (i.e. point ids are identical in the input and
            source).

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh from the 2D delaunay filter.

        Examples
        --------
        First, generate 30 points on circle and plot them.

        >>> import pyvista as pv
        >>> points = pv.Polygon(n_sides=30).points
        >>> circle = pv.PolyData(points)
        >>> circle.plot(show_edges=True, point_size=15)

        Use :func:`delaunay_2d` to fill the interior of the circle.

        >>> filled_circle = circle.delaunay_2d()
        >>> filled_circle.plot(show_edges=True, line_width=5)

        Use the ``edge_source`` parameter to create a constrained delaunay
        triangulation and plot it.

        >>> squar = pv.Polygon(n_sides=4, radius=8, fill=False)
        >>> squar = squar.rotate_z(45, inplace=False)
        >>> circ0 = pv.Polygon(center=(2, 3, 0), n_sides=30, radius=1)
        >>> circ1 = pv.Polygon(center=(-2, -3, 0), n_sides=30, radius=1)
        >>> comb = circ0 + circ1 + squar
        >>> tess = comb.delaunay_2d(edge_source=comb)
        >>> tess.plot(cpos='xy', show_edges=True)

        See :ref:`triangulated_surface` for more examples using this filter.

        """
        alg = _vtk.vtkDelaunay2D()
        alg.SetProjectionPlaneMode(_vtk.VTK_BEST_FITTING_PLANE)
        alg.SetInputDataObject(self)
        alg.SetTolerance(tol)
        alg.SetAlpha(alpha)
        alg.SetOffset(offset)
        alg.SetBoundingTriangulation(bound)
        if edge_source is not None:
            alg.SetSourceData(edge_source)
        _update_alg(alg, progress_bar, 'Computing 2D Triangulation')

        # Sometimes lines are given in the output. The
        # `.triangulate()` filter cleans those
        mesh = _get_output(alg).triangulate()
        if inplace:
            self.copy_from(mesh, deep=False)
            return self
        return mesh

    def compute_arc_length(self, progress_bar=False):
        """Compute the arc length over the length of the probed line.

        It adds a new point-data array named ``"arc_length"`` with the
        computed arc length for each of the polylines in the
        input. For all other cell types, the arc length is set to 0.

        Parameters
        ----------
        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        float
            Arc length of the length of the probed line.

        Examples
        --------
        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> path = sphere.geodesic(0, 100)
        >>> length = path.compute_arc_length()['arc_length'][-1]
        >>> f'Length is {length:.3f}'
        'Length is 0.812'

        This is identical to the geodesic_distance.

        >>> length = sphere.geodesic_distance(0, 100)
        >>> f'Length is {length:.3f}'
        'Length is 0.812'

        You can also plot the arc_length.

        >>> arc = path.compute_arc_length()
        >>> arc.plot(scalars="arc_length")

        """
        alg = _vtk.vtkAppendArcLength()
        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Computing the Arc Length')
        return _get_output(alg)

    def project_points_to_plane(self, origin=None, normal=(0.0, 0.0, 1.0), inplace=False):
        """Project points of this mesh to a plane.

        Parameters
        ----------
        origin : sequence[float], optional
            Plane origin.  Defaults to the approximate center of the
            input mesh minus half the length of the input mesh in the
            direction of the normal.

        normal : sequence[float], default: (0.0, 0.0, 1.0)
            Plane normal.  Defaults to +Z.

        inplace : bool, default: False
            Whether to overwrite the original mesh with the projected
            points.

        Returns
        -------
        pyvista.PolyData
            The points of this mesh projected onto a plane.

        Examples
        --------
        Flatten a sphere to the XY plane.

        >>> import pyvista as pv
        >>> sphere = pv.Sphere()
        >>> projected = sphere.project_points_to_plane()
        >>> projected.plot(show_edges=True, line_width=3)

        """
        if not isinstance(normal, (np.ndarray, collections.abc.Sequence)) or len(normal) != 3:
            raise TypeError('Normal must be a length three vector')
        if origin is None:
            origin = np.array(self.center) - np.array(normal) * self.length / 2.0
        # choose what mesh to use
        if not inplace:
            mesh = self.copy()
        else:
            mesh = self
        # Make plane
        plane = generate_plane(normal, origin)
        # Perform projection in place on the copied mesh
        f = lambda p: plane.ProjectPoint(p, p)
        np.apply_along_axis(f, 1, mesh.points)
        return mesh

    def ribbon(
        self,
        width=None,
        scalars=None,
        angle=0.0,
        factor=2.0,
        normal=None,
        tcoords=False,
        preference='points',
        progress_bar=False,
    ):
        """Create a ribbon of the lines in this dataset.

        .. note::
           If there are no lines in the input dataset, then the output
           will be an empty :class:`pyvista.PolyData` mesh.

        Parameters
        ----------
        width : float, optional
            Set the "half" width of the ribbon. If the width is
            allowed to vary, this is the minimum width. The default is
            10% the length.

        scalars : str, optional
            String name of the scalars array to use to vary the ribbon
            width.  This is only used if a scalars array is specified.

        angle : float, optional
            Angle in degrees of the offset angle of the ribbon from
            the line normal. The default is 0.0.

        factor : float, optional
            Set the maximum ribbon width in terms of a multiple of the
            minimum width. The default is 2.0.

        normal : sequence[float], optional
            Normal to use as default.

        tcoords : bool, str, optional
            If ``True``, generate texture coordinates along the
            ribbon. This can also be specified to generate the texture
            coordinates with either ``'length'`` or ``'normalized'``.

        preference : str, optional
            The field preference when searching for the scalars array by
            name.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Ribbon mesh.  Empty if there are no lines in the input dataset.

        Examples
        --------
        Convert a line to a ribbon and plot it.

        >>> import numpy as np
        >>> import pyvista
        >>> n = 1000
        >>> theta = np.linspace(-10 * np.pi, 10 * np.pi, n)
        >>> z = np.linspace(-2, 2, n)
        >>> r = z**2 + 1
        >>> x = r * np.sin(theta)
        >>> y = r * np.cos(theta)
        >>> points = np.column_stack((x, y, z))
        >>> pdata = pyvista.PolyData(points)
        >>> pdata.lines = np.hstack((n, range(n)))
        >>> pdata['distance'] = range(n)
        >>> ribbon = pdata.ribbon(width=0.2)
        >>> ribbon.plot(show_scalar_bar=False)

        """
        if scalars is not None:
            field = get_array_association(self, scalars, preference=preference)
        if width is None:
            width = self.length * 0.1
        alg = _vtk.vtkRibbonFilter()
        alg.SetInputDataObject(self)
        alg.SetWidth(width)
        if normal is not None:
            alg.SetUseDefaultNormal(True)
            alg.SetDefaultNormal(normal)
        alg.SetAngle(angle)
        if scalars is not None:
            alg.SetVaryWidth(True)
            alg.SetInputArrayToProcess(
                0, 0, 0, field.value, scalars
            )  # args: (idx, port, connection, field, name)
            alg.SetWidthFactor(factor)
        else:
            alg.SetVaryWidth(False)
        if tcoords:
            alg.SetGenerateTCoords(True)
            if isinstance(tcoords, str):
                if tcoords.lower() == 'length':
                    alg.SetGenerateTCoordsToUseLength()
                elif tcoords.lower() == 'normalized':
                    alg.SetGenerateTCoordsToNormalizedLength()
            else:
                alg.SetGenerateTCoordsToUseLength()
        else:
            alg.SetGenerateTCoordsToOff()
        _update_alg(alg, progress_bar, 'Creating a Ribbon')
        return _get_output(alg)

    def extrude(self, vector, capping=None, inplace=False, progress_bar=False):
        """Sweep polygonal data creating a "skirt" from free edges.

        This will create a line from vertices.

        This takes polygonal data as input and generates polygonal
        data on output. The input dataset is swept according to some
        extrusion function and creates new polygonal primitives. These
        primitives form a "skirt" or swept surface. For example,
        sweeping a line results in a quadrilateral, and sweeping a
        triangle creates a "wedge".

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

        .. versionchanged:: 0.32.0
           The ``capping`` keyword was added with a default of ``False``.
           The previously used VTK default corresponds to ``capping=True``.
           In a future version the default will be changed to ``True`` to
           match the behavior of the underlying VTK filter.

        Parameters
        ----------
        vector : numpy.ndarray or sequence
            Direction and length to extrude the mesh in.

        capping : bool, optional
            Control if the sweep of a 2D object is capped. The default is
            ``False``, which differs from VTK's default.

            .. warning::
               The ``capping`` keyword was added in version 0.32.0 with a
               default value of ``False``. In a future version this default
               will be changed to ``True`` to match the behavior of the
               underlying VTK filter. It is recommended to explicitly pass
               a value for this keyword argument to prevent future changes
               in behavior and warnings.

        inplace : bool, default: False
            Overwrites the original mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Extruded mesh.

        Examples
        --------
        Extrude a half circle arc.

        >>> import pyvista
        >>> arc = pyvista.CircularArc([-1, 0, 0], [1, 0, 0], [0, 0, 0])
        >>> mesh = arc.extrude([0, 0, 1], capping=False)
        >>> mesh.plot(color='tan')

        Extrude and cap an 8 sided polygon.

        >>> poly = pyvista.Polygon(n_sides=8)
        >>> mesh = poly.extrude((0, 0, 1.5), capping=True)
        >>> mesh.plot(line_width=5, show_edges=True)

        """
        if capping is None:
            capping = False
            warnings.warn(
                'The default value of the ``capping`` keyword argument will change in '
                'a future version to ``True`` to match the behavior of VTK. We recommend '
                'passing the keyword explicitly to prevent future surprises.',
                PyVistaFutureWarning,
            )

        alg = _vtk.vtkLinearExtrusionFilter()
        alg.SetExtrusionTypeToVectorExtrusion()
        alg.SetVector(*vector)
        alg.SetInputData(self)
        alg.SetCapping(capping)
        _update_alg(alg, progress_bar, 'Extruding')
        output = _get_output(alg)
        if inplace:
            self.copy_from(output, deep=False)
            return self
        return output

    def extrude_rotate(
        self,
        resolution=30,
        inplace=False,
        translation=0.0,
        dradius=0.0,
        angle=360.0,
        capping=None,
        rotation_axis=(0, 0, 1),
        progress_bar=False,
    ):
        """Sweep polygonal data creating "skirt" from free edges and lines, and lines from vertices.

        This takes polygonal data as input and generates polygonal
        data on output. The input dataset is swept around the axis
        to create new polygonal primitives. These primitives form a
        "skirt" or swept surface. For example, sweeping a line results
        in a cylindrical shell, and sweeping a circle creates a torus.

        There are a number of control parameters for this filter.  You
        can control whether the sweep of a 2D object (i.e., polygon or
        triangle strip) is capped with the generating geometry via the
        ``capping`` parameter. Also, you can control the angle of
        rotation, and whether translation along the axis is
        performed along with the rotation.  (Translation is useful for
        creating "springs".) You also can adjust the radius of the
        generating geometry with the ``dradius`` parameter.

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

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

        .. versionchanged:: 0.32.0
           The ``capping`` keyword was added with a default of ``False``.
           The previously used VTK default corresponds to ``capping=True``.
           In a future version the default will be changed to ``True`` to
           match the behavior of the underlying VTK filter.

        Parameters
        ----------
        resolution : int, optional
            Number of pieces to divide line into.

        inplace : bool, default: False
            Overwrites the original mesh inplace.

        translation : float, optional
            Total amount of translation along the axis.

        dradius : float, optional
            Change in radius during sweep process.

        angle : float, optional
            The angle of rotation in degrees.

        capping : bool, optional
            Control if the sweep of a 2D object is capped. The default is
            ``False``, which differs from VTK's default.

            .. warning::
               The ``capping`` keyword was added in version 0.32.0 with a
               default value of ``False``. In a future version this default
               will be changed to ``True`` to match the behavior of the
               underlying VTK filter. It is recommended to explicitly pass
               a value for this keyword argument to prevent future changes
               in behavior and warnings.

        rotation_axis : numpy.ndarray or sequence, optional
            The direction vector of the axis around which the rotation is done.
            It requires vtk>=9.1.0.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Rotationally extruded mesh.

        Examples
        --------
        Create a "spring" using the rotational extrusion filter.

        >>> import pyvista
        >>> profile = pyvista.Polygon(
        ...     center=[1.25, 0.0, 0.0],
        ...     radius=0.2,
        ...     normal=(0, 1, 0),
        ...     n_sides=30,
        ... )
        >>> extruded = profile.extrude_rotate(
        ...     resolution=360,
        ...     translation=4.0,
        ...     dradius=0.5,
        ...     angle=1500.0,
        ...     capping=True,
        ... )
        >>> extruded.plot(smooth_shading=True)

        Create a "wine glass" using the rotational extrusion filter.

        >>> import numpy as np
        >>> points = np.array(
        ...     [
        ...         [-0.18, 0, 0],
        ...         [-0.18, 0, 0.01],
        ...         [-0.18, 0, 0.02],
        ...         [-0.01, 0, 0.03],
        ...         [-0.01, 0, 0.04],
        ...         [-0.02, 0, 0.5],
        ...         [-0.05, 0, 0.75],
        ...         [-0.1, 0, 0.8],
        ...         [-0.2, 0, 1.0],
        ...     ]
        ... )
        >>> spline = pyvista.Spline(points, 30)
        >>> extruded = spline.extrude_rotate(resolution=20, capping=False)
        >>> extruded.plot(color='tan')

        """
        if capping is None:
            capping = False
            warnings.warn(
                'The default value of the ``capping`` keyword argument will change in '
                'a future version to ``True`` to match the behavior of VTK. We recommend '
                'passing the keyword explicitly to prevent future surprises.',
                PyVistaFutureWarning,
            )

        if (
            not isinstance(rotation_axis, (np.ndarray, collections.abc.Sequence))
            or len(rotation_axis) != 3
        ):
            raise ValueError('Vector must be a length three vector')

        if resolution <= 0:
            raise ValueError('`resolution` should be positive')
        alg = _vtk.vtkRotationalExtrusionFilter()
        alg.SetInputData(self)
        alg.SetResolution(resolution)
        alg.SetTranslation(translation)
        alg.SetDeltaRadius(dradius)
        alg.SetCapping(capping)
        alg.SetAngle(angle)
        if pyvista.vtk_version_info >= (9, 1, 0):
            alg.SetRotationAxis(rotation_axis)
        else:  # pragma: no cover
            if rotation_axis != (0, 0, 1):
                raise VTKVersionError(
                    'The installed version of VTK does not support '
                    'setting the direction vector of the axis around which the rotation is done.'
                )

        _update_alg(alg, progress_bar, 'Extruding')
        output = pyvista.wrap(alg.GetOutput())
        if inplace:
            self.copy_from(output, deep=False)
            return self
        return output

    def extrude_trim(
        self,
        direction,
        trim_surface,
        extrusion="boundary_edges",
        capping="intersection",
        inplace=False,
        progress_bar=False,
    ):
        """Extrude polygonal data trimmed by a surface.

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

        Parameters
        ----------
        direction : numpy.ndarray or sequence
            Direction vector to extrude.

        trim_surface : pyvista.PolyData
            Surface which trims the surface.

        extrusion : str, default: "boundary_edges"
            Control the strategy of extrusion. One of the following:

            * ``"boundary_edges"``
            * ``"all_edges"``

            The default only generates faces on the boundary of the original
            input surface. When using ``"all_edges"``, faces are created along
            interior points as well.

        capping : str, default: "intersection"
            Control the strategy of capping. One of the following:

            * ``"intersection"``
            * ``"minimum_distance"``
            * ``"maximum_distance"``
            * ``"average_distance"``

        inplace : bool, default: False
            Overwrites the original mesh in-place.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Extruded mesh trimmed by a surface.

        Examples
        --------
        Extrude a disc.

        >>> import pyvista
        >>> import numpy as np
        >>> plane = pyvista.Plane(
        ...     i_size=2, j_size=2, direction=[0, 0.8, 1]
        ... )
        >>> disc = pyvista.Disc(center=(0, 0, -1), c_res=50)
        >>> direction = [0, 0, 1]
        >>> extruded_disc = disc.extrude_trim(direction, plane)
        >>> extruded_disc.plot(smooth_shading=True, split_sharp_edges=True)

        """
        if not isinstance(direction, (np.ndarray, collections.abc.Sequence)) or len(direction) != 3:
            raise TypeError('Vector must be a length three vector')

        extrusions = {"boundary_edges": 0, "all_edges": 1}
        if isinstance(extrusion, str):
            if extrusion not in extrusions:
                raise ValueError(f'Invalid strategy of extrusion "{extrusion}".')
            extrusion = extrusions[extrusion]
        else:
            raise TypeError('Invalid type given to `extrusion`. Must be a string.')

        cappings = {
            "intersection": 0,
            "minimum_distance": 1,
            "maximum_distance": 2,
            "average_distance": 3,
        }
        if isinstance(capping, str):
            if capping not in cappings:
                raise ValueError(f'Invalid strategy of capping "{capping}".')
            capping = cappings[capping]
        else:
            raise TypeError('Invalid type given to `capping`. Must be a string.')

        alg = _vtk.vtkTrimmedExtrusionFilter()
        alg.SetInputData(self)
        alg.SetExtrusionDirection(*direction)
        alg.SetTrimSurfaceData(trim_surface)
        alg.SetExtrusionStrategy(extrusion)
        alg.SetCappingStrategy(capping)
        _update_alg(alg, progress_bar, 'Extruding with trimming')
        output = pyvista.wrap(alg.GetOutput())
        if inplace:
            self.copy_from(output, deep=False)
            return self
        return output

    def strip(
        self,
        join=False,
        max_length=1000,
        pass_cell_data=False,
        pass_cell_ids=False,
        pass_point_ids=False,
        progress_bar=False,
    ):
        """Strip poly data cells.

        Generates triangle strips and/or poly-lines from input
        polygons, triangle strips, and lines.

        Polygons are assembled into triangle strips only if they are
        triangles; other types of polygons are passed through to the
        output and not stripped. (Use ``triangulate`` filter to
        triangulate non-triangular polygons prior to running this
        filter if you need to strip all the data.) The filter will
        pass through (to the output) vertices if they are present in
        the input polydata.

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

        This filter implements `vtkStripper
        <https://vtk.org/doc/nightly/html/classvtkStripper.html>`_

        Parameters
        ----------
        join : bool, default: False
            If ``True``, the output polygonal segments will be joined
            if they are contiguous. This is useful after slicing a
            surface.

        max_length : int, default: 1000
            Specify the maximum number of triangles in a triangle
            strip, and/or the maximum number of lines in a poly-line.

        pass_cell_data : bool, default: False
            Enable/Disable passing of the CellData in the input to the
            output as FieldData. Note the field data is transformed.

        pass_cell_ids : bool, default: False
            If ``True``, the output polygonal dataset will have a
            celldata array that holds the cell index of the original
            3D cell that produced each output cell. This is useful for
            picking. The default is ``False`` to conserve memory.

        pass_point_ids : bool, default: False
            If ``True``, the output polygonal dataset will have a
            pointdata array that holds the point index of the original
            vertex that produced each output vertex. This is useful
            for picking. The default is ``False`` to conserve memory.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Stripped mesh.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()
        >>> slc = mesh.slice(normal='z', origin=(0, 0, -10))
        >>> stripped = slc.strip()
        >>> stripped.n_cells
        1
        >>> stripped.plot(show_edges=True, line_width=3)

        """
        alg = _vtk.vtkStripper()
        alg.SetInputDataObject(self)
        alg.SetJoinContiguousSegments(join)
        alg.SetMaximumLength(max_length)
        alg.SetPassCellDataAsFieldData(pass_cell_data)
        alg.SetPassThroughCellIds(pass_cell_ids)
        alg.SetPassThroughPointIds(pass_point_ids)
        _update_alg(alg, progress_bar, 'Stripping Mesh')
        return _get_output(alg)

    def collision(
        self,
        other_mesh,
        contact_mode=0,
        box_tolerance=0.001,
        cell_tolerance=0.0,
        n_cells_per_node=2,
        generate_scalars=False,
        progress_bar=False,
    ):
        """Perform collision determination between two polyhedral surfaces.

        If ``collision_mode`` is set to all contacts, the output will
        be lines of contact. If ``collision_mode`` is first contact or half
        contacts then the Contacts output will be vertices.

        .. warning::
            Currently only triangles are processed. Use
            :func:`PolyDataFilters.triangulate` to convert any strips
            or polygons to triangles.  Otherwise, the mesh will be
            converted for you within this method.

        Parameters
        ----------
        other_mesh : pyvista.DataSet
            Other mesh to test collision with.  If the other mesh is
            not a surface, its external surface will be extracted and
            triangulated.

        contact_mode : int, default: 0
            Contact mode.  One of the following:

            * 0 - All contacts. Find all the contacting cell pairs
              with two points per collision
            * 1 - First contact. Quickly find the first contact point.
            * 2 - Half contacts. Find all the contacting cell pairs
              with one point per collision.

        box_tolerance : float, default: 0.001
             Oriented bounding box (OBB) tree tolerance in world coordinates.

        cell_tolerance : float, default: 0.0
            Cell tolerance (squared value).

        n_cells_per_node : int, default: 2
            Number of cells in each OBB.

        generate_scalars : bool, default: False
            Flag to visualize the contact cells.  If ``True``, the
            contacting cells will be colored from red through blue,
            with collisions first determined colored red.  This array
            is stored as ``"collision_rgba"``.

            .. note::
               This will remove any other cell arrays in the mesh.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Mesh containing collisions in the ``field_data``
            attribute named ``"ContactCells"``.  Array only exists
            when there are collisions.

        int
            Number of collisions.

        Notes
        -----
        Due to the nature of the `vtk.vtkCollisionDetectionFilter
        <https://vtk.org/doc/nightly/html/classvtkCollisionDetectionFilter.html>`_,
        repeated uses of this method will be slower that using the
        ``vtk.vtkCollisionDetectionFilter`` directly.  The first
        update of the filter creates two instances of `vtkOBBTree
        <https://vtk.org/doc/nightly/html/classvtkOBBTree.html>`_,
        which can be subsequently updated by modifying the transform or
        matrix of the input meshes.

        This method assumes no transform and is easier to use for
        single collision tests, but it is recommended to use a
        combination of ``pyvista`` and ``vtk`` for rapidly computing
        repeated collisions.  See the `Collision Detection Example
        <https://kitware.github.io/vtk-examples/site/Python/Visualization/CollisionDetection/>`_

        Examples
        --------
        Compute the collision between a sphere and the back faces of a
        cube and output the cell indices of the first 10 collisions.

        >>> import numpy as np
        >>> import pyvista
        >>> mesh_a = pyvista.Sphere(radius=0.5)
        >>> mesh_b = pyvista.Cube((0.5, 0.5, 0.5)).extract_cells([0, 2, 4])
        >>> collision, ncol = mesh_a.collision(mesh_b, cell_tolerance=1)
        >>> collision['ContactCells'][:10]
        pyvista_ndarray([471, 471, 468, 468, 469, 469, 466, 466, 467, 467])

        Plot the collisions by creating a collision mask with the
        ``"ContactCells"`` field data.  Cells with a collision are
        colored red.

        >>> scalars = np.zeros(collision.n_cells, dtype=bool)
        >>> scalars[collision.field_data['ContactCells']] = True
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(
        ...     collision,
        ...     scalars=scalars,
        ...     show_scalar_bar=False,
        ...     cmap='bwr',
        ... )
        >>> _ = pl.add_mesh(
        ...     mesh_b,
        ...     color='tan',
        ...     line_width=5,
        ...     opacity=0.7,
        ...     show_edges=True,
        ... )
        >>> pl.show()

        Alternatively, simply plot the collisions using the default
        ``'collision_rgba'`` array after enabling ``generate_scalars``.

        >>> collision, ncol = mesh_a.collision(
        ...     mesh_b, cell_tolerance=1, generate_scalars=True
        ... )
        >>> collision.plot()

        See :ref:`collision_example` for more examples using this filter.

        """
        # other mesh must be a polydata
        if not isinstance(other_mesh, pyvista.PolyData):
            other_mesh = other_mesh.extract_surface()

        # according to VTK limitations
        poly_data = self
        if not poly_data.is_all_triangles:
            poly_data = poly_data.triangulate()
        if not other_mesh.is_all_triangles:
            other_mesh = other_mesh.triangulate()

        alg = _vtk.vtkCollisionDetectionFilter()
        alg.SetInputData(0, poly_data)
        alg.SetTransform(0, _vtk.vtkTransform())
        alg.SetInputData(1, other_mesh)
        alg.SetMatrix(1, _vtk.vtkMatrix4x4())
        alg.SetBoxTolerance(box_tolerance)
        alg.SetCellTolerance(cell_tolerance)
        alg.SetNumberOfCellsPerNode(n_cells_per_node)
        alg.SetCollisionMode(contact_mode)
        alg.SetGenerateScalars(generate_scalars)
        _update_alg(alg, progress_bar, 'Computing collisions')

        output = _get_output(alg)

        if generate_scalars:
            # must rename array as VTK sets the cell scalars array name to
            # a nullptr.
            # See https://github.com/pyvista/pyvista/pull/1540
            #
            # Note: Since all other cell arrays are destroyed when
            # generate_scalars is True, we can always index the first cell
            # array.
            output.cell_data.GetAbstractArray(0).SetName('collision_rgba')

        return output, alg.GetNumberOfContacts()

    def contour_banded(
        self,
        n_contours,
        rng=None,
        scalars=None,
        component=0,
        clip_tolerance=1e-6,
        generate_contour_edges=True,
        scalar_mode="value",
        clipping=True,
        progress_bar=False,
    ):
        """Generate filled contours.

        Generates filled contours for vtkPolyData. Filled contours are
        bands of cells that all have the same cell scalar value, and can
        therefore be colored the same. The method is also referred to as
        filled contour generation.

        This filter implements `vtkBandedPolyDataContourFilter
        <https://vtk.org/doc/nightly/html/classvtkBandedPolyDataContourFilter.html>`_.

        Parameters
        ----------
        n_contours : int
            Number of contours.

        rng : Sequence, optional
            Range of the scalars. Optional and defaults to the minimum and
            maximum of the active scalars of ``scalars``.

        scalars : str, optional
            The name of the scalar array to use for contouring.  If ``None``,
            the active scalar array will be used.

        component : int, default: 0
            The component to use of an input scalars array with more than one
            component.

        clip_tolerance : float, default: 1e-6
            Set/Get the clip tolerance.  Warning: setting this too large will
            certainly cause numerical issues. Change from the default value at
            your own risk. The actual internal clip tolerance is computed by
            multiplying ``clip_tolerance`` by the scalar range.

        generate_contour_edges : bool, default: True
            Controls whether contour edges are generated.  Contour edges are
            the edges between bands. If enabled, they are generated from
            polygons/triangle strips and returned as a second output.

        scalar_mode : str, default: 'value'
            Control whether the cell scalars are output as an integer index or
            a scalar value.  If ``'index'``, the index refers to the bands
            produced by the clipping range. If ``'value'``, then a scalar value
            which is a value between clip values is used.

        clipping : bool, default: True
            Indicate whether to clip outside ``rng`` and only return cells with
            values within ``rng``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        output : pyvista.PolyData
            Surface containing the contour surface.

        edges : pyvista.PolyData
            Optional edges when ``generate_contour_edges`` is ``True``.

        Examples
        --------
        Plot the random hills dataset and with 8 contour lines. Note how we use 7
        colors here (``n_contours - 1``).

        >>> import pyvista as pv
        >>> from pyvista import examples

        >>> mesh = examples.load_random_hills()
        >>> n_contours = 8
        >>> _, edges = mesh.contour_banded(n_contours)

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(
        ...     edges,
        ...     line_width=5,
        ...     render_lines_as_tubes=True,
        ...     color='k',
        ... )
        >>> _ = pl.add_mesh(mesh, n_colors=n_contours - 1, cmap='Set3')
        >>> pl.show()

        Extract the surface from the uniform grid dataset and plot its contours
        alongside the output from the banded contour filter.

        >>> surf = examples.load_uniform().extract_surface()
        >>> n_contours = 5
        >>> rng = [200, 500]
        >>> output, edges = surf.contour_banded(n_contours, rng=rng)

        >>> dargs = dict(n_colors=n_contours - 1, clim=rng)
        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(
        ...     edges,
        ...     line_width=5,
        ...     render_lines_as_tubes=True,
        ...     color='k',
        ... )
        >>> _ = pl.add_mesh(surf, opacity=0.3, **dargs)
        >>> _ = pl.add_mesh(output, **dargs)
        >>> pl.show()

        """
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            if self.point_data.active_scalars_name is None:
                raise MissingDataError('No point scalars to contour.')
            scalars = self.active_scalars_name
        arr = get_array(self, scalars, preference='point', err=False)
        if arr is None:
            raise ValueError('No arrays present to contour.')
        field = get_array_association(self, scalars, preference='point')
        if field != FieldAssociation.POINT:
            raise ValueError('Only point data can be contoured.')

        if rng is None:
            rng = (self.active_scalars.min(), self.active_scalars.max())

        alg = _vtk.vtkBandedPolyDataContourFilter()
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        alg.GenerateValues(n_contours, rng[0], rng[1])
        alg.SetInputDataObject(self)
        alg.SetClipping(clipping)
        if scalar_mode == 'value':
            alg.SetScalarModeToValue()
        elif scalar_mode == 'index':
            alg.SetScalarModeToIndex()
        else:
            raise ValueError(
                f'Invalid scalar mode "{scalar_mode}". Should be either "value" or "index".'
            )
        alg.SetGenerateContourEdges(generate_contour_edges)
        alg.SetClipTolerance(clip_tolerance)
        alg.SetComponent(component)
        _update_alg(alg, progress_bar, 'Contouring Mesh')
        mesh = _get_output(alg)

        # Must rename array as VTK sets the active scalars array name to a nullptr.
        # Please note this was fixed upstream in https://gitlab.kitware.com/vtk/vtk/-/merge_requests/9840
        for i in range(mesh.GetPointData().GetNumberOfArrays()):
            array = mesh.GetPointData().GetAbstractArray(i)
            name = array.GetName()
            if name is None:
                array.SetName(self.point_data.active_scalars_name)
        for i in range(mesh.GetCellData().GetNumberOfArrays()):
            array = mesh.GetCellData().GetAbstractArray(i)
            name = array.GetName()
            if name is None:
                array.SetName(self.cell_data.active_scalars_name)

        if generate_contour_edges:
            return mesh, pyvista.wrap(alg.GetContourEdgesOutput())
        return mesh

    def reconstruct_surface(self, nbr_sz=None, sample_spacing=None, progress_bar=False):
        """Reconstruct a surface from the points in this dataset.

        This filter takes a list of points assumed to lie on the
        surface of a solid 3D object. A signed measure of the distance
        to the surface is computed and sampled on a regular grid. The
        grid can then be contoured at zero to extract the surface. The
        default values for neighborhood size and sample spacing should
        give reasonable results for most uses but can be set if
        desired.

        This is helpful when generating surfaces from point clouds and
        is more reliable than :func:`DataSetFilters.delaunay_3d`.

        Parameters
        ----------
        nbr_sz : int, optional
            Specify the number of neighbors each point has, used for
            estimating the local surface orientation.

            The default value of 20 should be fine for most
            applications, higher values can be specified if the spread
            of points is uneven. Values as low as 10 may yield
            adequate results for some surfaces. Higher values cause
            the algorithm to take longer and will cause
            errors on sharp boundaries.

        sample_spacing : float, optional
            The spacing of the 3D sampling grid.  If not set, a
            reasonable guess will be made.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.PolyData
            Reconstructed surface.

        Examples
        --------
        Create a point cloud out of a sphere and reconstruct a surface
        from it.

        >>> import pyvista as pv
        >>> points = pv.wrap(pv.Sphere().points)
        >>> surf = points.reconstruct_surface()

        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_mesh(points)
        >>> _ = pl.add_title('Point Cloud of 3D Surface')
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_mesh(surf, color=True, show_edges=True)
        >>> _ = pl.add_title('Reconstructed Surface')
        >>> pl.show()

        See :ref:`surface_reconstruction_example` for more examples
        using this filter.

        """
        alg = _vtk.vtkSurfaceReconstructionFilter()
        alg.SetInputDataObject(self)
        if nbr_sz is not None:
            alg.SetNeighborhoodSize(nbr_sz)
        if sample_spacing is not None:
            alg.SetSampleSpacing(sample_spacing)

        # connect using ports as this will be slightly faster
        mc = _vtk.vtkMarchingCubes()
        mc.SetComputeNormals(False)
        mc.SetComputeScalars(False)
        mc.SetComputeGradients(False)
        mc.SetInputConnection(alg.GetOutputPort())
        mc.SetValue(0, 0.0)
        _update_alg(mc, progress_bar, 'Reconstructing surface')
        surf = pyvista.wrap(mc.GetOutput())
        return surf


# pyvista/core/filters/rectilinear_grid.py

"""Filters module with the class to manage filters/algorithms for rectilinear grid datasets."""

import collections
from typing import Sequence, Union
import warnings

import numpy as np

from pyvista import _vtk, abstract_class
from pyvista.core.filters import _get_output, _update_alg
from pyvista.utilities import assert_empty_kwargs
from pyvista.utilities.misc import PyVistaDeprecationWarning


@abstract_class
class RectilinearGridFilters:
    """An internal class to manage filters/algorithms for rectilinear grid datasets."""

    # Note remove kwargs when removing deprecation for pass_cell_data
    def to_tetrahedra(
        self,
        tetra_per_cell: int = 5,
        mixed: Union[Sequence[int], bool] = False,
        pass_cell_ids: bool = True,
        pass_data: bool = True,
        progress_bar: bool = False,
        **kwargs,
    ):
        """Create a tetrahedral mesh structured grid.

        Parameters
        ----------
        tetra_per_cell : int, default: 5
            The number of tetrahedrons to divide each cell into. Can be
            either ``5``, ``6``, or ``12``. If ``mixed=True``, this value is
            overridden.

        mixed : str, bool, sequence, default: False
            When set, subdivides some cells into 5 and some cells into 12. Set
            to ``True`` to use the active cell scalars of the
            :class:`pyvista.RectilinearGrid` to be either 5 or 12 to
            determining the number of tetrahedra to generate per cell.

            When a sequence, uses these values to subdivide the cells. When a
            string uses a cell array rather than the active array to determine
            the number of tetrahedra to generate per cell.

        pass_cell_ids : bool, default: True
            Set to ``True`` to make the tetrahedra have scalar data indicating
            which cell they came from in the original
            :class:`pyvista.RectilinearGrid`. The name of this array is
            ``'vtkOriginalCellIds'`` within the ``cell_data``.

        pass_data : bool, default: True
            Set to ``True`` to make the tetrahedra mesh have the cell data from
            the original :class:`pyvista.RectilinearGrid`.  This uses
            ``pass_cell_ids=True`` internally. If ``True``, ``pass_cell_ids``
            will also be set to ``True``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        **kwargs : dict, optional
            Deprecated keyword argument ``pass_cell_data``.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid containing the tetrahedral cells.

        Examples
        --------
        Divide a rectangular grid into tetrahedrons. Each cell contains by
        default 5 tetrahedrons.

        First, create and plot the grid.

        >>> import numpy as np
        >>> import pyvista as pv
        >>> xrng = np.linspace(0, 1, 2)
        >>> yrng = np.linspace(0, 1, 2)
        >>> zrng = np.linspace(0, 2, 3)
        >>> grid = pv.RectilinearGrid(xrng, yrng, zrng)
        >>> grid.plot()

        Now, generate the tetrahedra plot in the exploded view of the cell.

        >>> tet_grid = grid.to_tetrahedra()
        >>> tet_grid.explode(factor=0.5).plot(show_edges=True)

        Take the same grid but divide the first cell into 5 cells and the other
        cell into 12 tetrahedrons per cell.

        >>> tet_grid = grid.to_tetrahedra(mixed=[5, 12])
        >>> tet_grid.explode(factor=0.5).plot(show_edges=True)

        """
        # Note remove this section when deprecation is done
        pass_cell_data = kwargs.pop("pass_cell_data", None)
        assert_empty_kwargs(**kwargs)
        if pass_cell_data is not None:
            warnings.warn(
                "pass_cell_data is a deprecated option, use pass_data", PyVistaDeprecationWarning
            )
            pass_data = pass_cell_data

        alg = _vtk.vtkRectilinearGridToTetrahedra()
        alg.SetRememberVoxelId(pass_cell_ids or pass_data)
        if mixed is not False:
            if isinstance(mixed, str):
                self.cell_data.active_scalars_name = mixed
            elif isinstance(mixed, (np.ndarray, collections.abc.Sequence)):
                self.cell_data['_MIXED_CELLS_'] = mixed  # type: ignore
            elif not isinstance(mixed, bool):
                raise TypeError('`mixed` must be either a sequence of ints or bool')
            alg.SetTetraPerCellTo5And12()
        else:
            if tetra_per_cell not in [5, 6, 12]:
                raise ValueError(
                    f'`tetra_per_cell` should be either 5, 6, or 12, not {tetra_per_cell}'
                )

            # Edge case causing a seg-fault where grid is flat in one dimension
            # See: https://gitlab.kitware.com/vtk/vtk/-/issues/18650
            if 1 in self.dimensions and tetra_per_cell == 12:  # type: ignore
                raise RuntimeError(
                    'Cannot split cells into 12 tetrahedrals when at least '  # type: ignore
                    f'one dimension is 1. Dimensions are {self.dimensions}.'
                )

            alg.SetTetraPerCell(tetra_per_cell)

        alg.SetInputData(self)
        _update_alg(alg, progress_bar, 'Converting to tetrahedra')
        out = _get_output(alg)

        if pass_data:
            # algorithm stores original cell ids in active scalars
            # this does not preserve active scalars, but we need to
            # keep active scalars until they are renamed
            for name in self.cell_data:  # type: ignore
                if name != out.cell_data.active_scalars_name:
                    out[name] = self.cell_data[name][out.cell_data.active_scalars]  # type: ignore

            for name in self.point_data:  # type: ignore
                out[name] = self.point_data[name]  # type: ignore

        if alg.GetRememberVoxelId():
            # original cell_ids are not named and are the active scalars
            out.cell_data.set_array(
                out.cell_data.pop(out.cell_data.active_scalars_name), 'vtkOriginalCellIds'
            )

        if pass_data:
            # Now reset active scalars in cast the original mesh had data with active scalars
            association, name = self.active_scalars_info  # type: ignore
            out.set_active_scalars(name, preference=association)

        return out


# pyvista/core/filters/structured_grid.py

"""Filters module with class to manage filters/algorithms for structured grid datasets."""
import numpy as np

import pyvista
from pyvista import _vtk, abstract_class
from pyvista.core.filters import _get_output
from pyvista.core.filters.data_set import DataSetFilters


@abstract_class
class StructuredGridFilters(DataSetFilters):
    """An internal class to manage filters/algorithms for structured grid datasets."""

    def extract_subset(self, voi, rate=(1, 1, 1), boundary=False):
        """Select piece (e.g., volume of interest).

        To use this filter set the VOI ivar which are i-j-k min/max
        indices that specify a rectangular region in the data. (Note
        that these are 0-offset.) You can also specify a sampling rate
        to subsample the data.

        Typical applications of this filter are to extract a slice
        from a volume for image processing, subsampling large volumes
        to reduce data size, or extracting regions of a volume with
        interesting data.

        Parameters
        ----------
        voi : sequence[int]
            Length 6 iterable of ints: ``(xmin, xmax, ymin, ymax, zmin, zmax)``.
            These bounds specify the volume of interest in i-j-k min/max
            indices.

        rate : sequence[int], default: (1, 1, 1)
            Length 3 iterable of ints: ``(xrate, yrate, zrate)``.

        boundary : bool, default: False
            Control whether to enforce that the "boundary" of the grid
            is output in the subsampling process. (This only has
            effect when the rate in any direction is not equal to
            1). When this is on, the subsampling will always include
            the boundary of the grid even if the sample rate is
            not an even multiple of the grid dimensions.

        Returns
        -------
        pyvista.StructuredGrid
            StructuredGrid with extracted subset.

        Examples
        --------
        Split a grid in half.

        >>> import numpy as np
        >>> import pyvista
        >>> from pyvista import examples
        >>> grid = examples.load_structured()
        >>> voi_1 = grid.extract_subset(
        ...     [0, 80, 0, 40, 0, 1], boundary=True
        ... )
        >>> voi_2 = grid.extract_subset(
        ...     [0, 80, 40, 80, 0, 1], boundary=True
        ... )

        For fun, add the two grids back together and show they are
        identical to the original grid.

        >>> joined = voi_1.concatenate(voi_2, axis=1)
        >>> assert np.allclose(grid.points, joined.points)

        """
        alg = _vtk.vtkExtractGrid()
        alg.SetVOI(voi)
        alg.SetInputDataObject(self)
        alg.SetSampleRate(rate)
        alg.SetIncludeBoundary(boundary)
        alg.Update()
        return _get_output(alg)

    def concatenate(self, other, axis, tolerance=0.0):
        """Concatenate a structured grid to this grid.

        Joins structured grids into a single structured grid.  Grids
        must be of compatible dimension, and must be coincident along
        the seam. Grids must have the same point and cell data.  Field
        data is ignored.

        Parameters
        ----------
        other : pyvista.StructuredGrid
            Structured grid to concatenate.

        axis : int
            Axis along which to concatenate.

        tolerance : float, default: 0.0
            Tolerance for point coincidence along joining seam.

        Returns
        -------
        pyvista.StructuredGrid
            Concatenated grid.

        Examples
        --------
        Split a grid in half and join them.

        >>> import numpy as np
        >>> import pyvista
        >>> from pyvista import examples
        >>> grid = examples.load_structured()
        >>> voi_1 = grid.extract_subset(
        ...     [0, 80, 0, 40, 0, 1], boundary=True
        ... )
        >>> voi_2 = grid.extract_subset(
        ...     [0, 80, 40, 80, 0, 1], boundary=True
        ... )
        >>> joined = voi_1.concatenate(voi_2, axis=1)
        >>> f'{grid.dimensions} same as {joined.dimensions}'
        '(80, 80, 1) same as (80, 80, 1)'

        """
        if axis > 2:
            raise RuntimeError('Concatenation axis must be <= 2.')

        # check dimensions are compatible
        for i, (dim1, dim2) in enumerate(zip(self.dimensions, other.dimensions)):
            if i == axis:
                continue
            if dim1 != dim2:
                raise ValueError(
                    f'StructuredGrids with dimensions {self.dimensions} and {other.dimensions} '
                    'are not compatible.'
                )

        # check point/cell variables are the same
        if not set(self.point_data.keys()) == set(other.point_data.keys()):
            raise RuntimeError('Grid to concatenate has different point array names.')
        if not set(self.cell_data.keys()) == set(other.cell_data.keys()):
            raise RuntimeError('Grid to concatenate has different cell array names.')

        # check that points are coincident (within tolerance) along seam
        if not np.allclose(
            np.take(self.points_matrix, indices=-1, axis=axis),
            np.take(other.points_matrix, indices=0, axis=axis),
            atol=tolerance,
        ):
            raise RuntimeError(
                f'Grids cannot be joined along axis {axis}, as points '
                'are not coincident within tolerance of {tolerance}.'
            )

        # slice to cut off the repeated grid face
        slice_spec = [slice(None, None, None)] * 3
        slice_spec[axis] = slice(0, -1, None)
        slice_spec = tuple(slice_spec)  # trigger basic indexing

        # concatenate points, cutting off duplicate
        new_points = np.concatenate(
            (self.points_matrix[slice_spec], other.points_matrix), axis=axis
        )

        # concatenate point arrays, cutting off duplicate
        new_point_data = {}
        for name, point_array in self.point_data.items():
            arr_1 = self._reshape_point_array(point_array)
            arr_2 = other._reshape_point_array(other.point_data[name])
            if not np.array_equal(
                np.take(arr_1, indices=-1, axis=axis), np.take(arr_2, indices=0, axis=axis)
            ):
                raise RuntimeError(
                    f'Grids cannot be joined along axis {axis}, as field '
                    '`{name}` is not identical along the seam.'
                )
            new_point_data[name] = np.concatenate((arr_1[slice_spec], arr_2), axis=axis).ravel(
                order='F'
            )

        new_dims = np.array(self.dimensions)
        new_dims[axis] += other.dimensions[axis] - 1

        # concatenate cell arrays
        new_cell_data = {}
        for name, cell_array in self.cell_data.items():
            arr_1 = self._reshape_cell_array(cell_array)
            arr_2 = other._reshape_cell_array(other.cell_data[name])
            new_cell_data[name] = np.concatenate((arr_1, arr_2), axis=axis).ravel(order='F')

        # assemble output
        joined = pyvista.StructuredGrid()
        joined.dimensions = list(new_dims)
        joined.points = new_points.reshape((-1, 3), order='F')
        joined.point_data.update(new_point_data)
        joined.cell_data.update(new_cell_data)

        return joined


# pyvista/core/filters/uniform_grid.py

"""Filters with a class to manage filters/algorithms for uniform grid datasets."""
import collections.abc

import numpy as np

import pyvista
from pyvista import _vtk, abstract_class
from pyvista.core.filters import _get_output, _update_alg
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.errors import AmbiguousDataError, MissingDataError


@abstract_class
class UniformGridFilters(DataSetFilters):
    """An internal class to manage filters/algorithms for uniform grid datasets."""

    def gaussian_smooth(self, radius_factor=1.5, std_dev=2.0, scalars=None, progress_bar=False):
        """Smooth the data with a Gaussian kernel.

        Parameters
        ----------
        radius_factor : float | sequence[float], default: 1.5
            Unitless factor to limit the extent of the kernel.

        std_dev : float | sequence[float], default: 2.0
            Standard deviation of the kernel in pixel units.

        scalars : str, optional
            Name of scalars to process. Defaults to currently active scalars.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            Uniform grid with smoothed scalars.

        Notes
        -----
        This filter only supports point data. Consider converting any cell data
        to point data using the :func:`cell_data_to_point_data()
        <pyvista.DataSetFilters.cell_data_to_point_data>` filter to convert any
        cell data to point data.

        Examples
        --------
        First, create sample data to smooth. Here, we use
        :func:`pyvista.perlin_noise() <pyvista.utilities.common.perlin_noise>`
        to create meaningful data.

        >>> import numpy as np
        >>> import pyvista
        >>> noise = pyvista.perlin_noise(0.1, (2, 5, 8), (0, 0, 0))
        >>> grid = pyvista.sample_function(
        ...     noise, [0, 1, 0, 1, 0, 1], dim=(20, 20, 20)
        ... )
        >>> grid.plot(show_scalar_bar=False)

        Next, smooth the sample data.

        >>> smoothed = grid.gaussian_smooth()
        >>> smoothed.plot(show_scalar_bar=False)

        See :ref:`gaussian_smoothing_example` for a full example using this filter.

        """
        alg = _vtk.vtkImageGaussianSmooth()
        alg.SetInputDataObject(self)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
            if field.value == 1:
                raise ValueError('If `scalars` not given, active scalars must be point array.')
        else:
            field = self.get_array_association(scalars, preference='point')
            if field.value == 1:
                raise ValueError('Can only process point data, given `scalars` are cell data.')
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        if isinstance(radius_factor, collections.abc.Iterable):
            alg.SetRadiusFactors(radius_factor)
        else:
            alg.SetRadiusFactors(radius_factor, radius_factor, radius_factor)
        if isinstance(std_dev, collections.abc.Iterable):
            alg.SetStandardDeviations(std_dev)
        else:
            alg.SetStandardDeviations(std_dev, std_dev, std_dev)
        _update_alg(alg, progress_bar, 'Performing Gaussian Smoothing')
        return _get_output(alg)

    def median_smooth(
        self, kernel_size=(3, 3, 3), scalars=None, preference='point', progress_bar=False
    ):
        """Smooth data using a median filter.

        The Median filter that replaces each pixel with the median value from a
        rectangular neighborhood around that pixel. Neighborhoods can be no
        more than 3 dimensional. Setting one axis of the neighborhood
        kernelSize to 1 changes the filter into a 2D median.

        See `vtkImageMedian3D
        <https://vtk.org/doc/nightly/html/classvtkImageMedian3D.html#details>`_
        for more details.

        Parameters
        ----------
        kernel_size : sequence[int], default: (3, 3, 3)
            Size of the kernel in each dimension (units of voxels), for example
            ``(x_size, y_size, z_size)``. Default is a 3D median filter. If you
            want to do a 2D median filter, set the size to 1 in the dimension
            you don't want to filter over.

        scalars : str, optional
            Name of scalars to process. Defaults to currently active scalars.

        preference : str, default: "point"
            When scalars is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'`` or ``'cell'``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            Uniform grid with smoothed scalars.

        Warnings
        --------
        Applying this filter to cell data will send the output to a new point
        array with the same name, overwriting any existing point data array
        with the same name.

        Examples
        --------
        First, create sample data to smooth. Here, we use
        :func:`pyvista.perlin_noise() <pyvista.utilities.common.perlin_noise>`
        to create meaningful data.

        >>> import numpy as np
        >>> import pyvista
        >>> noise = pyvista.perlin_noise(0.1, (2, 5, 8), (0, 0, 0))
        >>> grid = pyvista.sample_function(
        ...     noise, [0, 1, 0, 1, 0, 1], dim=(20, 20, 20)
        ... )
        >>> grid.plot(show_scalar_bar=False)

        Next, smooth the sample data.

        >>> smoothed = grid.median_smooth(kernel_size=(10, 10, 10))
        >>> smoothed.plot(show_scalar_bar=False)

        """
        alg = _vtk.vtkImageMedian3D()
        alg.SetInputDataObject(self)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        else:
            field = self.get_array_association(scalars, preference=preference)
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        alg.SetKernelSize(kernel_size[0], kernel_size[1], kernel_size[2])
        _update_alg(alg, progress_bar, 'Performing Median Smoothing')
        return _get_output(alg)

    def extract_subset(self, voi, rate=(1, 1, 1), boundary=False, progress_bar=False):
        """Select piece (e.g., volume of interest).

        To use this filter set the VOI ivar which are i-j-k min/max indices
        that specify a rectangular region in the data. (Note that these are
        0-offset.) You can also specify a sampling rate to subsample the
        data.

        Typical applications of this filter are to extract a slice from a
        volume for image processing, subsampling large volumes to reduce data
        size, or extracting regions of a volume with interesting data.

        Parameters
        ----------
        voi : sequence[int]
            Length 6 iterable of ints: ``(xmin, xmax, ymin, ymax, zmin, zmax)``.
            These bounds specify the volume of interest in i-j-k min/max
            indices.

        rate : sequence[int], default: (1, 1, 1)
            Length 3 iterable of ints: ``(xrate, yrate, zrate)``.

        boundary : bool, default: False
            Control whether to enforce that the "boundary" of the grid
            is output in the subsampling process. This only has effect
            when the rate in any direction is not equal to 1. When
            this is enabled, the subsampling will always include the
            boundary of the grid even though the sample rate is not an
            even multiple of the grid dimensions. By default this is
            disabled.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            UniformGrid subset.
        """
        alg = _vtk.vtkExtractVOI()
        alg.SetVOI(voi)
        alg.SetInputDataObject(self)
        alg.SetSampleRate(rate)
        alg.SetIncludeBoundary(boundary)
        _update_alg(alg, progress_bar, 'Extracting Subset')
        result = _get_output(alg)
        # Adjust for the confusing issue with the extents
        #   see https://gitlab.kitware.com/vtk/vtk/-/issues/17938
        fixed = pyvista.UniformGrid()
        fixed.origin = result.bounds[::2]
        fixed.spacing = result.spacing
        fixed.dimensions = result.dimensions
        fixed.point_data.update(result.point_data)
        fixed.cell_data.update(result.cell_data)
        fixed.field_data.update(result.field_data)
        fixed.copy_meta_from(result, deep=True)
        return fixed

    def image_dilate_erode(
        self,
        dilate_value=1.0,
        erode_value=0.0,
        kernel_size=(3, 3, 3),
        scalars=None,
        progress_bar=False,
    ):
        """Dilates one value and erodes another.

        ``image_dilate_erode`` will dilate one value and erode another. It uses
        an elliptical footprint, and only erodes/dilates on the boundary of the
        two values. The filter is restricted to the X, Y, and Z axes for now.
        It can degenerate to a 2 or 1-dimensional filter by setting the kernel
        size to 1 for a specific axis.

        Parameters
        ----------
        dilate_value : float, default: 1.0
            Dilate value in the dataset.

        erode_value : float, default: 0.0
            Erode value in the dataset.

        kernel_size : sequence[int], default: (3, 3, 3)
            Determines the size of the kernel along the three axes.

        scalars : str, optional
            Name of scalars to process. Defaults to currently active scalars.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            Dataset that has been dilated/eroded on the boundary of the specified scalars.

        Notes
        -----
        This filter only supports point data. Consider converting any cell data
        to point data using the :func:`cell_data_to_point_data()
        <pyvista.DataSetFilters.cell_data_to_point_data>` filter to convert ny
        cell data to point data.

        Examples
        --------
        Demonstrate image dilate/erode on an example dataset. First, plot
        the example dataset with the active scalars.

        >>> from pyvista import examples
        >>> uni = examples.load_uniform()
        >>> uni.plot()

        Now, plot the image threshold with ``threshold=[400, 600]``. Note how
        values within the threshold are 1 and outside are 0.

        >>> ithresh = uni.image_threshold([400, 600])
        >>> ithresh.plot()

        Note how there is a hole in the thresholded image. Apply a dilation/
        erosion filter with a large kernel to fill that hole in.

        >>> idilate = ithresh.image_dilate_erode(kernel_size=[5, 5, 5])
        >>> idilate.plot()

        """
        alg = _vtk.vtkImageDilateErode3D()
        alg.SetInputDataObject(self)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
            if field.value == 1:
                raise ValueError('If `scalars` not given, active scalars must be point array.')
        else:
            field = self.get_array_association(scalars, preference='point')
            if field.value == 1:
                raise ValueError('Can only process point data, given `scalars` are cell data.')
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        alg.SetKernelSize(*kernel_size)
        alg.SetDilateValue(dilate_value)
        alg.SetErodeValue(erode_value)
        _update_alg(alg, progress_bar, 'Performing Dilation and Erosion')
        return _get_output(alg)

    def image_threshold(
        self,
        threshold,
        in_value=1.0,
        out_value=0.0,
        scalars=None,
        preference='point',
        progress_bar=False,
    ):
        """Apply a threshold to scalar values in a uniform grid.

        If a single value is given for threshold, scalar values above or equal
        to the threshold are ``'in'`` and scalar values below the threshold are ``'out'``.
        If two values are given for threshold (sequence) then values equal to
        or between the two values are ``'in'`` and values outside the range are ``'out'``.

        If ``None`` is given for ``in_value``, scalars that are ``'in'`` will not be replaced.
        If ``None`` is given for ``out_value``, scalars that are ``'out'`` will not be replaced.

        Warning: applying this filter to cell data will send the output to a
        new point array with the same name, overwriting any existing point data
        array with the same name.

        Parameters
        ----------
        threshold : float or sequence[float]
            Single value or (min, max) to be used for the data threshold.  If
            a sequence, then length must be 2. Threshold(s) for deciding which
            cells/points are ``'in'`` or ``'out'`` based on scalar data.

        in_value : float, default: 1.0
            Scalars that match the threshold criteria for ``'in'`` will be replaced with this.

        out_value : float, default: 0.0
            Scalars that match the threshold criteria for ``'out'`` will be replaced with this.

        scalars : str, optional
            Name of scalars to process. Defaults to currently active scalars.

        preference : str, default: "point"
            When scalars is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'`` or ``'cell'``.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            Dataset with the specified scalars thresholded.

        Examples
        --------
        Demonstrate image threshold on an example dataset. First, plot
        the example dataset with the active scalars.

        >>> from pyvista import examples
        >>> uni = examples.load_uniform()
        >>> uni.plot()

        Now, plot the image threshold with ``threshold=100``. Note how
        values above the threshold are 1 and below are 0.

        >>> ithresh = uni.image_threshold(100)
        >>> ithresh.plot()

        """
        alg = _vtk.vtkImageThreshold()
        alg.SetInputDataObject(self)
        if scalars is None:
            pyvista.set_default_active_scalars(self)
            field, scalars = self.active_scalars_info
        else:
            field = self.get_array_association(scalars, preference=preference)
        alg.SetInputArrayToProcess(
            0, 0, 0, field.value, scalars
        )  # args: (idx, port, connection, field, name)
        # set the threshold(s) and mode
        if isinstance(threshold, (np.ndarray, collections.abc.Sequence)):
            if len(threshold) != 2:
                raise ValueError(
                    f'Threshold must be length one for a float value or two for min/max; not ({threshold}).'
                )
            alg.ThresholdBetween(threshold[0], threshold[1])
        elif isinstance(threshold, collections.abc.Iterable):
            raise TypeError('Threshold must either be a single scalar or a sequence.')
        else:
            alg.ThresholdByUpper(threshold)
        # set the replacement values / modes
        if in_value is not None:
            alg.SetReplaceIn(True)
            alg.SetInValue(in_value)
        else:
            alg.SetReplaceIn(False)
        if out_value is not None:
            alg.SetReplaceOut(True)
            alg.SetOutValue(out_value)
        else:
            alg.SetReplaceOut(False)
        # run the algorithm
        _update_alg(alg, progress_bar, 'Performing Image Thresholding')
        return _get_output(alg)

    def fft(self, output_scalars_name=None, progress_bar=False):
        """Apply a fast Fourier transform (FFT) to the active scalars.

        The input can be real or complex data, but the output is always
        :attr:`numpy.complex128`. The filter is fastest for images that have
        power of two sizes.

        The filter uses a butterfly diagram for each prime factor of the
        dimension. This makes images with prime number dimensions (i.e. 17x17)
        much slower to compute. FFTs of multidimensional meshes (i.e volumes)
        are decomposed so that each axis executes serially.

        The frequencies of the output assume standard order: along each axis
        first positive frequencies are assumed from 0 to the maximum, then
        negative frequencies are listed from the largest absolute value to
        smallest. This implies that the corners of the grid correspond to low
        frequencies, while the center of the grid corresponds to high
        frequencies.

        Parameters
        ----------
        output_scalars_name : str, optional
            The name of the output scalars. By default, this is the same as the
            active scalars of the dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            :class:`pyvista.UniformGrid` with applied FFT.

        See Also
        --------
        rfft : The reverse transform.
        low_pass : Low-pass filtering of FFT output.
        high_pass : High-pass filtering of FFT output.

        Examples
        --------
        Apply FFT to an example image.

        >>> from pyvista import examples
        >>> image = examples.download_moonlanding_image()
        >>> fft_image = image.fft()
        >>> fft_image.point_data  # doctest:+SKIP
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : PNGImage
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
        PNGImage                complex128 (298620,)          SCALARS

        See :ref:`image_fft_example` for a full example using this filter.

        """
        # check for active scalars, otherwise risk of segfault
        if self.point_data.active_scalars_name is None:
            try:
                pyvista.set_default_active_scalars(self)
            except MissingDataError:
                raise MissingDataError('FFT filter requires point scalars.') from None

            # possible only cell scalars were made active
            if self.point_data.active_scalars_name is None:
                raise MissingDataError('FFT filter requires point scalars.')

        alg = _vtk.vtkImageFFT()
        alg.SetInputDataObject(self)
        _update_alg(alg, progress_bar, 'Performing Fast Fourier Transform')
        output = _get_output(alg)
        self._change_fft_output_scalars(
            output, self.point_data.active_scalars_name, output_scalars_name
        )
        return output

    def rfft(self, output_scalars_name=None, progress_bar=False):
        """Apply a reverse fast Fourier transform (RFFT) to the active scalars.

        The input can be real or complex data, but the output is always
        :attr:`numpy.complex128`. The filter is fastest for images that have power
        of two sizes.

        The filter uses a butterfly diagram for each prime factor of the
        dimension. This makes images with prime number dimensions (i.e. 17x17)
        much slower to compute. FFTs of multidimensional meshes (i.e volumes)
        are decomposed so that each axis executes serially.

        The frequencies of the input assume standard order: along each axis
        first positive frequencies are assumed from 0 to the maximum, then
        negative frequencies are listed from the largest absolute value to
        smallest. This implies that the corners of the grid correspond to low
        frequencies, while the center of the grid corresponds to high
        frequencies.

        Parameters
        ----------
        output_scalars_name : str, optional
            The name of the output scalars. By default, this is the same as the
            active scalars of the dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            :class:`pyvista.UniformGrid` with the applied reverse FFT.

        See Also
        --------
        fft : The direct transform.
        low_pass : Low-pass filtering of FFT output.
        high_pass : High-pass filtering of FFT output.

        Examples
        --------
        Apply reverse FFT to an example image.

        >>> from pyvista import examples
        >>> image = examples.download_moonlanding_image()
        >>> fft_image = image.fft()
        >>> image_again = fft_image.rfft()
        >>> image_again.point_data  # doctest:+SKIP
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : PNGImage
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            PNGImage                complex128 (298620,)            SCALARS

        See :ref:`image_fft_example` for a full example using this filter.

        """
        self._check_fft_scalars()
        alg = _vtk.vtkImageRFFT()
        alg.SetInputDataObject(self)
        _update_alg(alg, progress_bar, 'Performing Reverse Fast Fourier Transform.')
        output = _get_output(alg)
        self._change_fft_output_scalars(
            output, self.point_data.active_scalars_name, output_scalars_name
        )
        return output

    def low_pass(
        self,
        x_cutoff,
        y_cutoff,
        z_cutoff,
        order=1,
        output_scalars_name=None,
        progress_bar=False,
    ):
        """Perform a Butterworth low pass filter in the frequency domain.

        This filter requires that the :class:`UniformGrid` have a complex point
        scalars, usually generated after the :class:`UniformGrid` has been
        converted to the frequency domain by a :func:`UniformGridFilters.fft`
        filter.

        A :func:`UniformGridFilters.rfft` filter can be used to convert the
        output back into the spatial domain. This filter attenuates high
        frequency components.  Input and output are complex arrays with
        datatype :attr:`numpy.complex128`.

        The frequencies of the input assume standard order: along each axis
        first positive frequencies are assumed from 0 to the maximum, then
        negative frequencies are listed from the largest absolute value to
        smallest. This implies that the corners of the grid correspond to low
        frequencies, while the center of the grid corresponds to high
        frequencies.

        Parameters
        ----------
        x_cutoff : float
            The cutoff frequency for the x axis.

        y_cutoff : float
            The cutoff frequency for the y axis.

        z_cutoff : float
            The cutoff frequency for the z axis.

        order : int, default: 1
            The order of the cutoff curve. Given from the equation
            ``1 + (cutoff/freq(i, j))**(2*order)``.

        output_scalars_name : str, optional
            The name of the output scalars. By default, this is the same as the
            active scalars of the dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            :class:`pyvista.UniformGrid` with the applied low pass filter.

        See Also
        --------
        fft : Direct fast Fourier transform.
        rfft : Reverse fast Fourier transform.
        high_pass : High-pass filtering of FFT output.

        Examples
        --------
        See :ref:`image_fft_perlin_example` for a full example using this filter.

        """
        self._check_fft_scalars()
        alg = _vtk.vtkImageButterworthLowPass()
        alg.SetInputDataObject(self)
        alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff)
        alg.SetOrder(order)
        _update_alg(alg, progress_bar, 'Performing Low Pass Filter')
        output = _get_output(alg)
        self._change_fft_output_scalars(
            output, self.point_data.active_scalars_name, output_scalars_name
        )
        return output

    def high_pass(
        self,
        x_cutoff,
        y_cutoff,
        z_cutoff,
        order=1,
        output_scalars_name=None,
        progress_bar=False,
    ):
        """Perform a Butterworth high pass filter in the frequency domain.

        This filter requires that the :class:`UniformGrid` have a complex point
        scalars, usually generated after the :class:`UniformGrid` has been
        converted to the frequency domain by a :func:`UniformGridFilters.fft`
        filter.

        A :func:`UniformGridFilters.rfft` filter can be used to convert the
        output back into the spatial domain. This filter attenuates low
        frequency components.  Input and output are complex arrays with
        datatype :attr:`numpy.complex128`.

        The frequencies of the input assume standard order: along each axis
        first positive frequencies are assumed from 0 to the maximum, then
        negative frequencies are listed from the largest absolute value to
        smallest. This implies that the corners of the grid correspond to low
        frequencies, while the center of the grid corresponds to high
        frequencies.

        Parameters
        ----------
        x_cutoff : float
            The cutoff frequency for the x axis.

        y_cutoff : float
            The cutoff frequency for the y axis.

        z_cutoff : float
            The cutoff frequency for the z axis.

        order : int, default: 1
            The order of the cutoff curve. Given from the equation
            ``1/(1 + (cutoff/freq(i, j))**(2*order))``.

        output_scalars_name : str, optional
            The name of the output scalars. By default, this is the same as the
            active scalars of the dataset.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        pyvista.UniformGrid
            :class:`pyvista.UniformGrid` with the applied high pass filter.

        See Also
        --------
        fft : Direct fast Fourier transform.
        rfft : Reverse fast Fourier transform.
        low_pass : Low-pass filtering of FFT output.

        Examples
        --------
        See :ref:`image_fft_perlin_example` for a full example using this filter.

        """
        self._check_fft_scalars()
        alg = _vtk.vtkImageButterworthHighPass()
        alg.SetInputDataObject(self)
        alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff)
        alg.SetOrder(order)
        _update_alg(alg, progress_bar, 'Performing High Pass Filter')
        output = _get_output(alg)
        self._change_fft_output_scalars(
            output, self.point_data.active_scalars_name, output_scalars_name
        )
        return output

    def _change_fft_output_scalars(self, dataset, orig_name, out_name):
        """Modify the name and dtype of the output scalars for an FFT filter."""
        name = orig_name if out_name is None else out_name
        pdata = dataset.point_data
        if pdata.active_scalars_name != name:
            pdata[name] = pdata.pop(pdata.active_scalars_name)

        # always view the datatype of the point_data as complex128
        dataset._association_complex_names['POINT'].add(name)

    def _check_fft_scalars(self):
        """Check for complex active scalars.

        This is necessary for rfft, low_pass, and high_pass filters.

        """
        # check for complex active point scalars, otherwise the risk of segfault
        if self.point_data.active_scalars_name is None:
            possible_scalars = self.point_data.keys()
            if len(possible_scalars) == 1:
                self.set_active_scalars(possible_scalars[0], preference='point')
            elif len(possible_scalars) > 1:
                raise AmbiguousDataError(
                    'There are multiple point scalars available. Set one to be '
                    'active with `point_data.active_scalars_name = `'
                )
            else:
                raise MissingDataError('FFT filters require point scalars.')

        if not np.issubdtype(self.point_data.active_scalars.dtype, np.complexfloating):
            raise ValueError(
                'Active scalars must be complex data for this filter, represented '
                'as an array with a datatype of `numpy.complex64` or '
                '`numpy.complex128`.'
            )

    def _flip_uniform(self, axis) -> 'pyvista.UniformGrid':
        """Flip the uniform grid along a specified axis and return a uniform grid.

        This varies from :func:`DataSet.flip_x` because it returns a UniformGrid.

        """
        alg = _vtk.vtkImageFlip()
        alg.SetInputData(self)
        alg.SetFilteredAxes(axis)
        alg.Update()
        return pyvista.wrap(alg.GetOutput())


# pyvista/core/filters/unstructured_grid.py

"""Filters module with a class to manage filters/algorithms for unstructured grid datasets."""
from functools import wraps

from pyvista import _vtk, abstract_class
from pyvista.core.errors import VTKVersionError
from pyvista.core.filters import _get_output, _update_alg
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.core.filters.poly_data import PolyDataFilters


@abstract_class
class UnstructuredGridFilters(DataSetFilters):
    """An internal class to manage filters/algorithms for unstructured grid datasets."""

    @wraps(PolyDataFilters.delaunay_2d)
    def delaunay_2d(self, *args, **kwargs):
        """Wrap ``PolyDataFilters.delaunay_2d``."""
        return PolyDataFilters.delaunay_2d(self, *args, **kwargs)

    @wraps(PolyDataFilters.reconstruct_surface)
    def reconstruct_surface(self, *args, **kwargs):
        """Wrap ``PolyDataFilters.reconstruct_surface``."""
        return PolyDataFilters.reconstruct_surface(self, *args, **kwargs)

    def subdivide_tetra(self):
        """Subdivide each tetrahedron into twelve tetrahedrons.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid containing the subdivided tetrahedrons.

        Examples
        --------
        First, load a sample tetrahedral UnstructuredGrid and plot it.

        >>> from pyvista import examples
        >>> grid = examples.load_tetbeam()
        >>> grid.plot(show_edges=True, line_width=2)

        Now, subdivide and plot.

        >>> subdivided = grid.subdivide_tetra()
        >>> subdivided.plot(show_edges=True, line_width=2)

        """
        alg = _vtk.vtkSubdivideTetra()
        alg.SetInputData(self)
        _update_alg(alg)
        return _get_output(alg)

    def clean(
        self,
        tolerance=0,
        remove_unused_points=True,
        produce_merge_map=True,
        average_point_data=True,
        merging_array_name=None,
        progress_bar=False,
    ):
        """Merge duplicate points and remove unused points in an UnstructuredGrid.

        This filter, merging coincident points as defined by a merging
        tolerance and optionally removes unused points. The filter does not
        modify the topology of the input dataset, nor change the types of
        cells. It may however, renumber the cell connectivity ids.

        This filter implements `vtkStaticCleanUnstructuredGrid
        <https://vtk.org/doc/nightly/html/classvtkStaticCleanUnstructuredGrid.html>`_

        Parameters
        ----------
        tolerance : float, default: 0.0
            The absolute point merging tolerance.

        remove_unused_points : bool, default: True
            Indicate whether points unused by any cell are removed from the
            output. Note that when this is off, the filter can successfully
            process datasets with no cells (and just points). If on in this
            case, and there are no cells, the output will be empty.

        produce_merge_map : bool, default: False
            Indicate whether a merge map should be produced on output.
            The merge map, if requested, maps each input point to its
            output point id, or provides a value of -1 if the input point
            is not used in the output. The merge map is associated with
            the filter's output field data and is named ``"PointMergeMap"``.

        average_point_data : bool, default: True
            Indicate whether point coordinates and point data of merged points
            are averaged. When ``True``, the data coordinates and attribute
            values of all merged points are averaged. When ``False``, the point
            coordinate and data of the single remaining merged point is
            retained.

        merging_array_name : str, optional
            If a ``merging_array_name`` is specified and exists in the
            ``point_data``, then point merging will switch into a mode where
            merged points must be both geometrically coincident and have
            matching point data. When set, ``tolerance`` has no effect.

        progress_bar : bool, default: False
            Display a progress bar to indicate progress.

        Returns
        -------
        UnstructuredGrid
            Cleaned unstructured grid.

        Examples
        --------
        Demonstrate cleaning an UnstructuredGrid and show how it can be used to
        average the point data across merged points.

        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> hexbeam = examples.load_hexbeam()
        >>> hexbeam_shifted = hexbeam.translate([1, 0, 0])
        >>> hexbeam.point_data['data'] = [0] * hexbeam.n_points
        >>> hexbeam_shifted.point_data['data'] = [1] * hexbeam.n_points
        >>> merged = hexbeam.merge(hexbeam_shifted, merge_points=False)
        >>> cleaned = merged.clean(average_point_data=True)
        >>> cleaned.n_points < merged.n_points
        True

        Show how point averaging using the ``clean`` method with
        ``average_point_data=True`` results in averaged point data for merged
        points.

        >>> pl = pv.Plotter(shape=(1, 2))
        >>> _ = pl.add_mesh(merged, scalars='data', show_scalar_bar=False)
        >>> pl.subplot(0, 1)
        >>> _ = pl.add_mesh(cleaned, scalars='data')
        >>> pl.show()

        """
        try:
            from vtkmodules.vtkFiltersCore import vtkStaticCleanUnstructuredGrid
        except ImportError:  # pragma no cover
            raise VTKVersionError("UnstructuredGrid.clean requires VTK >= 9.2.2") from None

        alg = vtkStaticCleanUnstructuredGrid()
        alg.SetInputDataObject(self)
        alg.SetAbsoluteTolerance(True)
        alg.SetTolerance(tolerance)
        alg.SetMergingArray(merging_array_name)
        alg.SetRemoveUnusedPoints(remove_unused_points)
        alg.SetProduceMergeMap(produce_merge_map)
        alg.SetAveragePointData(average_point_data)
        _update_alg(alg, progress_bar, 'Cleaning Unstructured Grid')
        return _get_output(alg)


# pyvista/__init__.py

"""PyVista package for 3D plotting and mesh analysis."""

MAX_N_COLOR_BARS = 10

from typing import Optional
import warnings
import os

from pyvista._version import __version__
from pyvista.plotting import *
from pyvista.utilities import *
from pyvista.core import *
from pyvista.utilities.misc import (
    _get_vtk_id_type,
    vtk_version_info,
    _set_plot_theme_from_env,
    set_pickle_format,
)
from pyvista import _vtk
from pyvista.jupyter import set_jupyter_backend
from pyvista.themes import set_plot_theme, load_theme, _rcParams
from pyvista.themes import DefaultTheme as _GlobalTheme  # hide this

# Per contract with Sphinx-Gallery, this method must be available at top level
from pyvista.utilities.sphinx_gallery import _get_sg_image_scraper

from pyvista.utilities.wrappers import _wrappers

global_theme = _GlobalTheme()
rcParams = _rcParams()  # raises DeprecationError when used

# Set preferred plot theme
_set_plot_theme_from_env()

# get the int type from vtk
ID_TYPE = _get_vtk_id_type()

# determine if using at least vtk 9.0.0
if vtk_version_info.major < 9:  # pragma: no cover
    from pyvista.core.errors import VTKVersionError

    raise VTKVersionError('VTK version must be 9.0.0 or greater.')

# catch annoying numpy/vtk future warning:
warnings.simplefilter(action='ignore', category=FutureWarning)

# A simple flag to set when generating the documentation
OFF_SCREEN = os.environ.get("PYVISTA_OFF_SCREEN", "false").lower() == "true"

# flag for when building the sphinx_gallery
BUILDING_GALLERY = False
if 'PYVISTA_BUILDING_GALLERY' in os.environ:
    if os.environ['PYVISTA_BUILDING_GALLERY'].lower() == 'true':
        BUILDING_GALLERY = True

# A threshold for the max cells to compute a volume for when repr-ing
REPR_VOLUME_MAX_CELLS = 1e6

# Set where figures are saved
FIGURE_PATH = None

# Send VTK messages to the logging module:
send_errors_to_logging()

# theme to use by default for the plot directive
PLOT_DIRECTIVE_THEME = None

# Set a parameter to control default print format for floats outside
# of the plotter
FLOAT_FORMAT = "{:.3e}"

# Serialization format to be used when pickling `DataObject`
PICKLE_FORMAT = 'xml'

# Name used for unnamed scalars
DEFAULT_SCALARS_NAME = 'Data'


# pyvista/_version.py

"""Version info for pyvista.

On the ``main`` branch, use 'dev0' to denote a development version.
For example:

version_info = 0, 27, 'dev0'

When generating pre-release wheels, use '0rcN', for example:

version_info = 0, 28, '0rc1'

Denotes the first release candidate.

"""
# major, minor, patch
version_info = 0, 40, 'dev0'

# Nice string for the version
__version__ = '.'.join(map(str, version_info))


# pyvista/conftest.py

"""Close all plotters to help control memory usage for our doctests."""

import pytest

import pyvista


@pytest.fixture(autouse=True)
def autoclose_plotters():
    """Close all plotters."""
    yield
    pyvista.close_all()


@pytest.fixture(autouse=True)
def reset_gloal_theme():
    """Reset global_theme."""
    # this stops any doctest-module tests from overriding the global theme and
    # creating test side effects
    yield
    pyvista.global_theme.restore_defaults()


# pyvista/errors.py

"""PyVista specific errors."""


class MissingDataError(ValueError):
    """Exception when data is missing, e.g. no active scalars can be set."""

    def __init__(self, message='No data available.'):
        """Call the base class constructor with the custom message."""
        super().__init__(message)


class AmbiguousDataError(ValueError):
    """Exception when data is ambiguous, e.g. multiple active scalars can be set."""

    def __init__(self, message="Multiple data available."):
        """Call the base class constructor with the custom message."""
        super().__init__(message)


class PyVistaPipelineError(RuntimeError):
    """Exception when a VTK pipeline runs into an issue."""

    def __init__(self, message="VTK pipeline issue detected by PyVista."):
        """Call the base class constructor with the custom message."""
        super().__init__(message)


class RenderWindowUnavailable(RuntimeError):
    """Exception when the render window is not available."""

    def __init__(self, message="Render window is not available."):
        """Call the base class constructor with the custom message."""
        super().__init__(message)


# pyvista/themes.py

"""API description for managing plotting theme parameters in pyvista.

Examples
--------
Apply a built-in theme

>>> import pyvista as pv
>>> pv.set_plot_theme('default')
>>> pv.set_plot_theme('document')
>>> pv.set_plot_theme('dark')
>>> pv.set_plot_theme('paraview')

Load a theme into pyvista

>>> from pyvista.themes import DefaultTheme
>>> theme = DefaultTheme()
>>> theme.save('my_theme.json')  # doctest:+SKIP
>>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

Create a custom theme from the default theme and load it into
pyvista.

>>> my_theme = DefaultTheme()
>>> my_theme.font.size = 20
>>> my_theme.font.title_size = 40
>>> my_theme.cmap = 'jet'
>>> pv.global_theme.load_theme(my_theme)
>>> pv.global_theme.font.size
20

"""

from enum import Enum
import json
import os
from typing import Callable, List, Optional, Union
import warnings

from ._typing import ColorLike, Number
from .plotting.colors import Color, get_cmap_safe, get_cycler
from .plotting.opts import InterpolationType
from .plotting.plotting import Plotter
from .plotting.tools import parse_font_family
from .utilities.misc import PyVistaDeprecationWarning, _check_range


class _rcParams(dict):  # pragma: no cover
    """Reference to the deprecated rcParams dictionary."""

    def __getitem__(self, key):
        import pyvista  # avoids circular import

        warnings.warn(
            'rcParams is deprecated.  Please use ``pyvista.global_theme``.', DeprecationWarning
        )
        return getattr(pyvista.global_theme, key)

    def __setitem__(self, key, value):
        import pyvista  # avoids circular import

        warnings.warn(
            'rcParams is deprecated.  Please use ``pyvista.global_theme``.', DeprecationWarning
        )
        setattr(pyvista.global_theme, key, value)

    def __repr__(self):
        """Use the repr of global_theme."""
        import pyvista  # avoids circular import

        warnings.warn(
            'rcParams is deprecated.  Please use ``pyvista.global_theme``', DeprecationWarning
        )
        return repr(pyvista.global_theme)


def load_theme(filename):
    """Load a theme from a file.

    Parameters
    ----------
    filename : str
        Theme file. Must be json.

    Examples
    --------
    >>> import pyvista as pv
    >>> from pyvista.themes import DefaultTheme
    >>> theme = DefaultTheme()
    >>> theme.save('my_theme.json')  # doctest:+SKIP
    >>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

    """
    with open(filename) as f:
        theme_dict = json.load(f)
    return DefaultTheme.from_dict(theme_dict)


def set_plot_theme(theme):
    """Set the plotting parameters to a predefined theme using a string.

    Parameters
    ----------
    theme : str
        Theme name.  Either ``'default'``, ``'document'``, ``'dark'``,
        or ``'paraview'``.

    Examples
    --------
    Set to the default theme.

    >>> import pyvista as pv
    >>> pv.set_plot_theme('default')

    Set to the document theme.

    >>> pv.set_plot_theme('document')

    Set to the dark theme.

    >>> pv.set_plot_theme('dark')

    Set to the ParaView theme.

    >>> pv.set_plot_theme('paraview')

    """
    import pyvista

    if isinstance(theme, str):
        theme = theme.lower()
        try:
            new_theme_type = _NATIVE_THEMES[theme].value
        except KeyError:
            raise ValueError(f"Theme {theme} not found in PyVista's native themes.")
        pyvista.global_theme.load_theme(new_theme_type())
    elif isinstance(theme, DefaultTheme):
        pyvista.global_theme.load_theme(theme)
    else:
        raise TypeError(
            f'Expected a ``pyvista.themes.DefaultTheme`` or ``str``, not '
            f'a {type(theme).__name__}'
        )


class _ThemeConfig:
    """Provide common methods for theme configuration classes."""

    __slots__: List[str] = []

    @classmethod
    def from_dict(cls, dict_):
        """Create from a dictionary."""
        inst = cls()
        for key, value in dict_.items():
            attr = getattr(inst, key)
            if hasattr(attr, 'from_dict'):
                setattr(inst, key, attr.from_dict(value))
            else:
                setattr(inst, key, value)
        return inst

    def to_dict(self) -> dict:
        """Return theme config parameters as a dictionary.

        Returns
        -------
        dict
            This theme parameter represented as a dictionary.

        """
        # remove the first underscore in each entry
        dict_ = {}
        for key in self.__slots__:
            value = getattr(self, key)
            key = key[1:]
            if hasattr(value, 'to_dict'):
                dict_[key] = value.to_dict()
            else:
                dict_[key] = value
        return dict_

    def __eq__(self, other):
        if not isinstance(self, type(other)):
            return False

        for attr_name in other.__slots__:
            attr = getattr(self, attr_name)
            other_attr = getattr(other, attr_name)
            if isinstance(attr, (tuple, list)):
                if tuple(attr) != tuple(other_attr):
                    return False
            else:
                if not attr == other_attr:
                    return False

        return True

    def __getitem__(self, key):
        """Get a value via a key.

        Implemented here for backwards compatibility.
        """
        return getattr(self, key)

    def __setitem__(self, key, value):
        """Set a value via a key.

        Implemented here for backwards compatibility.
        """
        setattr(self, key, value)


class _LightingConfig(_ThemeConfig):
    """PyVista lighting configuration.

    This will control the lighting interpolation type, parameters,
    and Physically Based Rendering (PBR) options

    Examples
    --------
    Set global PBR parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.lighting_params.interpolation = 'pbr'
    >>> pv.global_theme.lighting_params.metallic = 0.5
    >>> pv.global_theme.lighting_params.roughness = 0.25

    """

    __slots__ = [
        '_interpolation',
        '_metallic',
        '_roughness',
        '_ambient',
        '_diffuse',
        '_specular',
        '_specular_power',
        '_emissive',
    ]

    def __init__(self):
        self._interpolation = InterpolationType.FLAT.value
        self._metallic = 0.0
        self._roughness = 0.5
        self._ambient = 0.0
        self._diffuse = 1.0
        self._specular = 0.0
        self._specular_power = 100.0
        self._emissive = False

    @property
    def interpolation(self) -> InterpolationType:
        """Return or set the default interpolation type.

        See :class:`pyvista.plotting.opts.InterpolationType`.

        Options are:

        * ``'Phong'``
        * ``'Flat'``
        * ``'Physically based rendering'``

        This is stored as an integer value of the ``InterpolationType``
        so that the theme can be JSON-serializable.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.interpolation = 'Phong'
        >>> pv.global_theme.lighting_params.interpolation
        <InterpolationType.PHONG: 2>


        """
        return InterpolationType.from_any(self._interpolation)

    @interpolation.setter
    def interpolation(self, interpolation: Union[str, int, InterpolationType]):
        self._interpolation = InterpolationType.from_any(interpolation).value

    @property
    def metallic(self) -> float:
        """Return or set the metallic value.

        This requires that the interpolation be set to ``'Physically based
        rendering'``. Must be between 0 and 1.

        Examples
        --------
        Set the global metallic value used in physically based rendering to
        ``0.5``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.metallic = 0.5
        >>> pv.global_theme.lighting_params.metallic
        0.5

        """
        return self._metallic

    @metallic.setter
    def metallic(self, metallic: float):
        _check_range(metallic, (0, 1), 'metallic')
        self._metallic = metallic

    @property
    def roughness(self) -> float:
        """Return or set the roughness value.

        This value has to be between 0 (glossy) and 1 (rough). A glossy
        material has reflections and a high specular part. This parameter is
        only used by PBR interpolation.

        Examples
        --------
        Set the global roughness value used in physically based rendering to
        ``0.25``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.roughness = 0.25
        >>> pv.global_theme.lighting_params.roughness
        0.25

        """
        return self._roughness

    @roughness.setter
    def roughness(self, roughness: float):
        _check_range(roughness, (0, 1), 'roughness')
        self._roughness = roughness

    @property
    def ambient(self) -> float:
        """Return or set the ambient value.

        When lighting is enabled, this is the amount of light in the range of 0
        to 1 that reaches the actor when not directed at the light source
        emitted from the viewer.

        Examples
        --------
        Set the global ambient lighting value to ``0.2``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.ambient = 0.2
        >>> pv.global_theme.lighting_params.ambient
        0.2

        """
        return self._ambient

    @ambient.setter
    def ambient(self, ambient: float):
        _check_range(ambient, (0, 1), 'ambient')
        self._ambient = ambient

    @property
    def diffuse(self) -> float:
        """Return or set the diffuse value.

        This is the scattering of light by reflection or
        transmission. Diffuse reflection results when light strikes an
        irregular surface such as a frosted window or the surface of a
        frosted or coated light bulb. Must be between 0 and 1.

        Examples
        --------
        Set the global diffuse lighting value to ``0.5``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.diffuse = 0.5
        >>> pv.global_theme.lighting_params.diffuse
        0.5

        """
        return self._diffuse

    @diffuse.setter
    def diffuse(self, diffuse: float):
        _check_range(diffuse, (0, 1), 'diffuse')
        self._diffuse = diffuse

    @property
    def specular(self) -> float:
        """Return or set the specular value.

        Specular lighting simulates the bright spot of a light that appears
        on shiny objects. Must be between 0 and 1.

        Examples
        --------
        Set the global specular value to ``0.1``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.specular = 0.1
        >>> pv.global_theme.lighting_params.specular
        0.1

        """
        return self._specular

    @specular.setter
    def specular(self, specular: float):
        _check_range(specular, (0, 1), 'specular')
        self._specular = specular

    @property
    def specular_power(self) -> float:
        """Return or set the specular power value.

        Must be between 0.0 and 128.0.

        Examples
        --------
        Set the global specular power value to ``50``.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.specular_power = 50
        >>> pv.global_theme.lighting_params.specular_power
        50

        """
        return self._specular_power

    @specular_power.setter
    def specular_power(self, specular_power: float):
        _check_range(specular_power, (0, 128), 'specular_power')
        self._specular_power = specular_power

    @property
    def emissive(self) -> bool:
        """Return or set if emissive is used with point Gaussian style.

        Examples
        --------
        Globally enable emissive lighting when using the point Gaussian style.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting_params.emissive = True
        >>> pv.global_theme.lighting_params.emissive
        True

        """
        return self._emissive

    @emissive.setter
    def emissive(self, emissive: bool):
        self._emissive = bool(emissive)


class _DepthPeelingConfig(_ThemeConfig):
    """PyVista depth peeling configuration.

    Examples
    --------
    Set global depth peeling parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.depth_peeling.number_of_peels = 1
    >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0
    >>> pv.global_theme.depth_peeling.enabled = True

    """

    __slots__ = ['_number_of_peels', '_occlusion_ratio', '_enabled']

    def __init__(self):
        self._number_of_peels = 4
        self._occlusion_ratio = 0.0
        self._enabled = False

    @property
    def number_of_peels(self) -> int:
        """Return or set the number of peels in depth peeling.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.number_of_peels = 1

        """
        return self._number_of_peels

    @number_of_peels.setter
    def number_of_peels(self, number_of_peels: int):
        self._number_of_peels = int(number_of_peels)

    @property
    def occlusion_ratio(self) -> float:
        """Return or set the occlusion ratio in depth peeling.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0

        """
        return self._occlusion_ratio

    @occlusion_ratio.setter
    def occlusion_ratio(self, occlusion_ratio: float):
        self._occlusion_ratio = float(occlusion_ratio)

    @property
    def enabled(self) -> bool:
        """Return or set if depth peeling is enabled.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.enabled = True

        """
        return self._enabled

    @enabled.setter
    def enabled(self, enabled: bool):
        self._enabled = bool(enabled)

    def __repr__(self):
        txt = ['']
        parm = {
            'Number': 'number_of_peels',
            'Occlusion ratio': 'occlusion_ratio',
            'Enabled': 'enabled',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)


class _SilhouetteConfig(_ThemeConfig):
    """PyVista silhouette configuration.

    Examples
    --------
    Set global silhouette parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.silhouette.enabled = True
    >>> pv.global_theme.silhouette.color = 'grey'
    >>> pv.global_theme.silhouette.line_width = 2
    >>> pv.global_theme.silhouette.feature_angle = 20

    """

    __slots__ = ['_color', '_line_width', '_opacity', '_feature_angle', '_decimate', '_enabled']

    def __init__(self):
        self._color = Color('black')
        self._line_width = 2
        self._opacity = 1.0
        self._feature_angle = None
        self._decimate = None
        self._enabled = False

    @property
    def enabled(self) -> bool:
        """Return or set whether silhouette is on or off."""
        return self._enabled

    @enabled.setter
    def enabled(self, enabled: bool):
        self._enabled = bool(enabled)

    @property
    def color(self) -> Color:
        """Return or set the silhouette color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.color = 'red'

        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def line_width(self) -> float:
        """Return or set the silhouette line width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.line_width = 2.0

        """
        return self._line_width

    @line_width.setter
    def line_width(self, line_width: float):
        self._line_width = float(line_width)

    @property
    def opacity(self) -> float:
        """Return or set the silhouette opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.opacity = 1.0

        """
        return self._opacity

    @opacity.setter
    def opacity(self, opacity: float):
        _check_range(opacity, (0, 1), 'opacity')
        self._opacity = float(opacity)

    @property
    def feature_angle(self) -> Union[float, None]:
        """Return or set the silhouette feature angle.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.feature_angle = 20.0

        """
        return self._feature_angle

    @feature_angle.setter
    def feature_angle(self, feature_angle: Union[float, None]):
        self._feature_angle = feature_angle

    @property
    def decimate(self) -> float:
        """Return or set the amount to decimate the silhouette.

        Parameter must be between 0 and 1.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.decimate = 0.9

        """
        return self._decimate

    @decimate.setter
    def decimate(self, decimate: float):
        if decimate is None:
            self._decimate = None
        else:
            _check_range(decimate, (0, 1), 'decimate')
            self._decimate = float(decimate)

    def __repr__(self):
        txt = ['']
        parm = {
            'Color': 'color',
            'Line width': 'line_width',
            'Opacity': 'opacity',
            'Feature angle': 'feature_angle',
            'Decimate': 'decimate',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)


class _ColorbarConfig(_ThemeConfig):
    """PyVista colorbar configuration.

    Examples
    --------
    Set the colorbar width.

    >>> import pyvista as pv
    >>> pv.global_theme.colorbar_horizontal.width = 0.2

    """

    __slots__ = ['_width', '_height', '_position_x', '_position_y']

    def __init__(self):
        self._width = None
        self._height = None
        self._position_x = None
        self._position_y = None

    @property
    def width(self) -> float:
        """Return or set colorbar width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.width = 0.2

        """
        return self._width

    @width.setter
    def width(self, width: float):
        self._width = float(width)

    @property
    def height(self) -> float:
        """Return or set colorbar height.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.height = 0.2

        """
        return self._height

    @height.setter
    def height(self, height: float):
        self._height = float(height)

    @property
    def position_x(self) -> float:
        """Return or set colorbar x position.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.position_x = 0.2

        """
        return self._position_x

    @position_x.setter
    def position_x(self, position_x: float):
        self._position_x = float(position_x)

    @property
    def position_y(self) -> float:
        """Return or set colorbar y position.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.position_y = 0.2

        """
        return self._position_y

    @position_y.setter
    def position_y(self, position_y: float):
        self._position_y = float(position_y)

    def __repr__(self):
        txt = ['']
        parm = {
            'Width': 'width',
            'Height': 'height',
            'X Position': 'position_x',
            'Y Position': 'position_y',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)


class _AxesConfig(_ThemeConfig):
    """PyVista axes configuration.

    Examples
    --------
    Set the x axis color to black.

    >>> import pyvista as pv
    >>> pv.global_theme.axes.x_color = 'black'

    Show axes by default.

    >>> pv.global_theme.axes.show = True

    Use the ``vtk.vtkCubeAxesActor``.

    >>> pv.global_theme.axes.box = True

    """

    __slots__ = ['_x_color', '_y_color', '_z_color', '_box', '_show']

    def __init__(self):
        self._x_color = Color('tomato')
        self._y_color = Color('seagreen')
        self._z_color = Color('mediumblue')
        self._box = False
        self._show = True

    def __repr__(self):
        txt = ['Axes configuration']
        parm = {
            'X Color': 'x_color',
            'Y Color': 'y_color',
            'Z Color': 'z_color',
            'Use Box': 'box',
            'Show': 'show',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)

    @property
    def x_color(self) -> Color:
        """Return or set x axis color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.axes.x_color = 'red'
        """
        return self._x_color

    @x_color.setter
    def x_color(self, color: ColorLike):
        self._x_color = Color(color)

    @property
    def y_color(self) -> Color:
        """Return or set y axis color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.axes.y_color = 'red'
        """
        return self._y_color

    @y_color.setter
    def y_color(self, color: ColorLike):
        self._y_color = Color(color)

    @property
    def z_color(self) -> Color:
        """Return or set z axis color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.axes.z_color = 'red'
        """
        return self._z_color

    @z_color.setter
    def z_color(self, color: ColorLike):
        self._z_color = Color(color)

    @property
    def box(self) -> bool:
        """Use the ``vtk.vtkCubeAxesActor`` instead of the default ``vtk.vtkAxesActor``.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.axes.box = True

        """
        return self._box

    @box.setter
    def box(self, box: bool):
        self._box = bool(box)

    @property
    def show(self) -> bool:
        """Show or hide the axes actor.

        Examples
        --------
        Hide the axes by default.

        >>> import pyvista as pv
        >>> pv.global_theme.axes.show = False

        """
        return self._show

    @show.setter
    def show(self, show: bool):
        self._show = bool(show)


class _Font(_ThemeConfig):
    """PyVista plotter font configuration.

    Examples
    --------
    Set the default font family to 'arial'.  Must be either
    'arial', 'courier', or 'times'.

    >>> import pyvista as pv
    >>> pv.global_theme.font.family = 'arial'

    Set the default font size to 20.

    >>> pv.global_theme.font.size = 20

    Set the default title size to 40

    >>> pv.global_theme.font.title_size = 40

    Set the default label size to 10

    >>> pv.global_theme.font.label_size = 10

    Set the default text color to 'grey'

    >>> pv.global_theme.font.color = 'grey'

    Set the string formatter used to format numerical data to '%.6e'

    >>> pv.global_theme.font.fmt = '%.6e'

    """

    __slots__ = ['_family', '_size', '_title_size', '_label_size', '_color', '_fmt']

    def __init__(self):
        self._family = 'arial'
        self._size = 12
        self._title_size = None
        self._label_size = None
        self._color = Color('white')
        self._fmt = None

    def __repr__(self):
        txt = ['']
        parm = {
            'Family': 'family',
            'Size': 'size',
            'Title size': 'title_size',
            'Label size': 'label_size',
            'Color': 'color',
            'Float format': 'fmt',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')

        return '\n'.join(txt)

    @property
    def family(self) -> str:
        """Return or set the font family.

        Must be one of the following:

        * ``"arial"``
        * ``"courier"``
        * ``"times"``

        Examples
        --------
        Set the default global font family to 'courier'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.family = 'courier'

        """
        return self._family

    @family.setter
    def family(self, family: str):
        parse_font_family(family)  # check valid font
        self._family = family

    @property
    def size(self) -> int:
        """Return or set the font size.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.size = 20

        """
        return self._size

    @size.setter
    def size(self, size: int):
        self._size = int(size)

    @property
    def title_size(self) -> int:
        """Return or set the title size.

        If ``None``, then VTK uses ``UnconstrainedFontSizeOn`` for titles.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.title_size = 20
        """
        return self._title_size

    @title_size.setter
    def title_size(self, title_size: int):
        if title_size is None:
            self._title_size = None
        else:
            self._title_size = int(title_size)

    @property
    def label_size(self) -> int:
        """Return or set the label size.

        If ``None``, then VTK uses ``UnconstrainedFontSizeOn`` for labels.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.label_size = 20
        """
        return self._label_size

    @label_size.setter
    def label_size(self, label_size: int):
        if label_size is None:
            self._label_size = None
        else:
            self._label_size = int(label_size)

    @property
    def color(self) -> Color:
        """Return or set the font color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.font.color = 'black'
        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def fmt(self) -> str:
        """Return or set the string formatter used to format numerical data.

        Examples
        --------
        Set the string formatter used to format numerical data to '%.6e'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.fmt = '%.6e'

        """
        return self._fmt

    @fmt.setter
    def fmt(self, fmt: str):
        self._fmt = fmt


class _SliderStyleConfig(_ThemeConfig):
    """PyVista configuration for a single slider style."""

    __slots__ = [
        '_name',
        '_slider_length',
        '_slider_width',
        '_slider_color',
        '_tube_width',
        '_tube_color',
        '_cap_opacity',
        '_cap_length',
        '_cap_width',
    ]

    def __init__(self):
        """Initialize the slider style configuration."""
        self._name = None
        self._slider_length = None
        self._slider_width = None
        self._slider_color = None
        self._tube_width = None
        self._tube_color = None
        self._cap_opacity = None
        self._cap_length = None
        self._cap_width = None

    @property
    def name(self) -> str:
        """Return the name of the slider style configuration."""
        return self._name

    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def cap_width(self) -> float:
        """Return or set the cap width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_width = 0.02

        """
        return self._cap_width

    @cap_width.setter
    def cap_width(self, cap_width: float):
        self._cap_width = float(cap_width)

    @property
    def cap_length(self) -> float:
        """Return or set the cap length.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_length = 0.01

        """
        return self._cap_length

    @cap_length.setter
    def cap_length(self, cap_length: float):
        self._cap_length = float(cap_length)

    @property
    def cap_opacity(self) -> float:
        """Return or set the cap opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.cap_opacity = 1.0

        """
        return self._cap_opacity

    @cap_opacity.setter
    def cap_opacity(self, cap_opacity: float):
        _check_range(cap_opacity, (0, 1), 'cap_opacity')
        self._cap_opacity = float(cap_opacity)

    @property
    def tube_color(self) -> Color:
        """Return or set the tube color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.tube_color = 'black'
        """
        return self._tube_color

    @tube_color.setter
    def tube_color(self, tube_color: ColorLike):
        self._tube_color = Color(tube_color)

    @property
    def tube_width(self) -> float:
        """Return or set the tube_width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.tube_width = 0.005

        """
        return self._tube_width

    @tube_width.setter
    def tube_width(self, tube_width: float):
        self._tube_width = float(tube_width)

    @property
    def slider_color(self) -> Color:
        """Return or set the slider color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_color = 'grey'

        """
        return self._slider_color

    @slider_color.setter
    def slider_color(self, slider_color: ColorLike):
        self._slider_color = Color(slider_color)

    @property
    def slider_width(self) -> float:
        """Return or set the slider width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_width = 0.04

        """
        return self._slider_width

    @slider_width.setter
    def slider_width(self, slider_width: float):
        self._slider_width = float(slider_width)

    @property
    def slider_length(self) -> float:
        """Return or set the slider_length.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.slider_styles.modern.slider_length = 0.02

        """
        return self._slider_length

    @slider_length.setter
    def slider_length(self, slider_length: float):
        self._slider_length = float(slider_length)

    def __repr__(self):
        txt = ['']
        parm = {
            'Slider length': 'slider_length',
            'Slider width': 'slider_width',
            'Slider color': 'slider_color',
            'Tube width': 'tube_width',
            'Tube color': 'tube_color',
            'Cap opacity': 'cap_opacity',
            'Cap length': 'cap_length',
            'Cap width': 'cap_width',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'        {name:<17}: {setting}')
        return '\n'.join(txt)


class _SliderConfig(_ThemeConfig):
    """PyVista configuration encompassing all slider styles.

    Examples
    --------
    Set the classic slider configuration.

    >>> import pyvista as pv
    >>> slider_styles = pv.global_theme.slider_styles
    >>> slider_styles.classic.slider_length = 0.02
    >>> slider_styles.classic.slider_width = 0.04
    >>> slider_styles.classic.slider_color = (0.5, 0.5, 0.5)
    >>> slider_styles.classic.tube_width = 0.005
    >>> slider_styles.classic.tube_color = (1.0, 1.0, 1.0)
    >>> slider_styles.classic.cap_opacity = 1
    >>> slider_styles.classic.cap_length = 0.01
    >>> slider_styles.classic.cap_width = 0.02

    Set the modern slider configuration.

    >>> slider_styles.modern.slider_length = 0.02
    >>> slider_styles.modern.slider_width = 0.04
    >>> slider_styles.modern.slider_color = (0.43, 0.44, 0.45)
    >>> slider_styles.modern.tube_width = 0.04
    >>> slider_styles.modern.tube_color = (0.69, 0.70, 0.709)
    >>> slider_styles.modern.cap_opacity = 0
    >>> slider_styles.modern.cap_length = 0.01
    >>> slider_styles.modern.cap_width = 0.02

    """

    __slots__ = ['_classic', '_modern']

    def __init__(self):
        """Initialize the slider configuration."""
        self._classic = _SliderStyleConfig()
        self._classic.name = 'classic'
        self._classic.slider_length = 0.02
        self._classic.slider_width = 0.04
        self._classic.slider_color = 'gray'
        self._classic.tube_width = 0.005
        self._classic.tube_color = 'white'
        self._classic.cap_opacity = 1
        self._classic.cap_length = 0.01
        self._classic.cap_width = 0.02

        self._modern = _SliderStyleConfig()
        self._modern.name = 'modern'
        self._modern.slider_length = 0.02
        self._modern.slider_width = 0.04
        self._modern.slider_color = (110, 113, 117)
        self._modern.tube_width = 0.04
        self._modern.tube_color = (178, 179, 181)
        self._modern.cap_opacity = 0
        self._modern.cap_length = 0.01
        self._modern.cap_width = 0.02

    @property
    def classic(self) -> _SliderStyleConfig:
        """Return the Classic slider configuration."""
        return self._classic

    @classic.setter
    def classic(self, config: _SliderStyleConfig):
        if not isinstance(config, _SliderStyleConfig):
            raise TypeError('Configuration type must be `_SliderStyleConfig`')
        self._classic = config

    @property
    def modern(self) -> _SliderStyleConfig:
        """Return the Modern slider configuration."""
        return self._modern

    @modern.setter
    def modern(self, config: _SliderStyleConfig):
        if not isinstance(config, _SliderStyleConfig):
            raise TypeError('Configuration type must be `_SliderStyleConfig`')
        self._modern = config

    def __repr__(self):
        txt = ['']
        parm = {
            'Classic': 'classic',
            'Modern': 'modern',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'    {name:<21}: {setting}')
        return '\n'.join(txt)

    def __iter__(self):
        for style in [self._classic, self._modern]:
            yield style.name


class _TrameConfig(_ThemeConfig):
    """PyVista Trame configuration.

    Examples
    --------
    Set global trame view parameters.

    >>> import pyvista as pv
    >>> pv.global_theme.trame.interactive_ratio = 2
    >>> pv.global_theme.trame.still_ratio = 2

    """

    __slots__ = [
        '_interactive_ratio',
        '_still_ratio',
        '_jupyter_server_name',
        '_jupyter_server_port',
        '_server_proxy_enabled',
        '_server_proxy_prefix',
        '_default_mode',
    ]

    def __init__(self):
        self._interactive_ratio = 1
        self._still_ratio = 1
        self._jupyter_server_name = 'pyvista-jupyter'
        self._jupyter_server_port = 0
        self._server_proxy_enabled = 'PYVISTA_TRAME_SERVER_PROXY_PREFIX' in os.environ
        # default for ``jupyter-server-proxy``
        service = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '')
        prefix = os.environ.get('PYVISTA_TRAME_SERVER_PROXY_PREFIX', '/proxy/')
        if service and not prefix.startswith('http'):  # pragma: no cover
            self._server_proxy_prefix = os.path.join(service, prefix.lstrip('/'))
        else:
            self._server_proxy_prefix = prefix
        self._default_mode = 'trame'

    @property
    def interactive_ratio(self) -> Number:
        """Return or set the interactive ratio for PyVista Trame views.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.trame.interactive_ratio = 2

        """
        return self._interactive_ratio

    @interactive_ratio.setter
    def interactive_ratio(self, interactive_ratio: Number):
        self._interactive_ratio = interactive_ratio

    @property
    def still_ratio(self) -> Number:
        """Return or set the still ratio for PyVista Trame views.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.trame.still_ratio = 2

        """
        return self._still_ratio

    @still_ratio.setter
    def still_ratio(self, still_ratio: Number):
        self._still_ratio = still_ratio

    @property
    def jupyter_server_name(self):
        """Return or set the trame server name PyVista uses in Jupyter.

        This defaults to ``'pyvista-jupyter'``.

        This must be set before running :func:`pyvista.set_jupyter_backend`
        to ensure a server of this name is launched.

        Most users should not need to modify this.

        """
        return self._jupyter_server_name

    @jupyter_server_name.setter
    def jupyter_server_name(self, name: str):
        self._jupyter_server_name = name

    @property
    def jupyter_server_port(self) -> int:
        """Return or set the port for the Trame Jupyter server."""
        return self._jupyter_server_port

    @jupyter_server_port.setter
    def jupyter_server_port(self, port: int):
        self._jupyter_server_port = port

    @property
    def server_proxy_enabled(self) -> bool:
        """Return or set if use of relative URLs is enabled for the Jupyter interface."""
        return self._server_proxy_enabled

    @server_proxy_enabled.setter
    def server_proxy_enabled(self, enabled: bool):
        self._server_proxy_enabled = bool(enabled)

    @property
    def server_proxy_prefix(self):
        """Return or set URL prefix when using relative URLs with the Jupyter interface."""
        return self._server_proxy_prefix

    @server_proxy_prefix.setter
    def server_proxy_prefix(self, prefix: str):
        self._server_proxy_prefix = prefix

    @property
    def default_mode(self):
        """Return or set the default mode of the Trame backend.

        * ``'trame'``: Uses a view that can switch between client and server
          rendering modes.
        * ``'server'``: Uses a view that is purely server rendering.
        * ``'client'``: Uses a view that is purely client rendering (generally
          safe without a virtual frame buffer)

        """
        return self._default_mode

    @default_mode.setter
    def default_mode(self, mode: str):
        self._default_mode = mode


class DefaultTheme(_ThemeConfig):
    """PyVista default theme.

    Examples
    --------
    Change the global default background color to white.

    >>> import pyvista as pv
    >>> pv.global_theme.color = 'white'

    Show edges by default.

    >>> pv.global_theme.show_edges = True

    Create a new theme from the default theme and apply it globally.

    >>> from pyvista.themes import DefaultTheme
    >>> my_theme = DefaultTheme()
    >>> my_theme.color = 'red'
    >>> my_theme.background = 'white'
    >>> pv.global_theme.load_theme(my_theme)

    """

    __slots__ = [
        '_name',
        '_background',
        '_jupyter_backend',
        '_trame',
        '_full_screen',
        '_window_size',
        '_image_scale',
        '_camera',
        '_notebook',
        '_font',
        '_auto_close',
        '_cmap',
        '_color',
        '_color_cycler',
        '_above_range_color',
        '_below_range_color',
        '_nan_color',
        '_edge_color',
        '_line_width',
        '_point_size',
        '_outline_color',
        '_floor_color',
        '_colorbar_orientation',
        '_colorbar_horizontal',
        '_colorbar_vertical',
        '_show_scalar_bar',
        '_show_edges',
        '_show_vertices',
        '_lighting',
        '_interactive',
        '_render_points_as_spheres',
        '_render_lines_as_tubes',
        '_transparent_background',
        '_title',
        '_axes',
        '_multi_samples',
        '_multi_rendering_splitting_position',
        '_volume_mapper',
        '_smooth_shading',
        '_depth_peeling',
        '_silhouette',
        '_slider_styles',
        '_return_cpos',
        '_hidden_line_removal',
        '_anti_aliasing',
        '_enable_camera_orientation_widget',
        '_split_sharp_edges',
        '_sharp_edges_feature_angle',
        '_before_close_callback',
        '_lighting_params',
        '_interpolate_before_map',
        '_opacity',
    ]

    def __init__(self):
        """Initialize the theme."""
        self._name = 'default'
        self._background = Color([0.3, 0.3, 0.3])
        self._full_screen = False
        self._camera = {
            'position': [1, 1, 1],
            'viewup': [0, 0, 1],
        }

        self._notebook = None
        self._window_size = [1024, 768]
        self._image_scale = 1
        self._font = _Font()
        self._cmap = 'viridis'
        self._color = Color('white')
        self._color_cycler = None
        self._nan_color = Color('darkgray')
        self._above_range_color = Color('grey')
        self._below_range_color = Color('grey')
        self._edge_color = Color('black')
        self._line_width = 1.0
        self._point_size = 5.0
        self._outline_color = Color('white')
        self._floor_color = Color('gray')
        self._colorbar_orientation = 'horizontal'

        self._colorbar_horizontal = _ColorbarConfig()
        self._colorbar_horizontal.width = 0.6
        self._colorbar_horizontal.height = 0.08
        self._colorbar_horizontal.position_x = 0.35
        self._colorbar_horizontal.position_y = 0.05

        self._colorbar_vertical = _ColorbarConfig()
        self._colorbar_vertical.width = 0.08
        self._colorbar_vertical.height = 0.45
        self._colorbar_vertical.position_x = 0.9
        self._colorbar_vertical.position_y = 0.02

        self._show_scalar_bar = True
        self._show_edges = False
        self._show_vertices = False
        self._lighting = True
        self._interactive = False
        self._render_points_as_spheres = False
        self._render_lines_as_tubes = False
        self._transparent_background = False
        self._title = 'PyVista'
        self._axes = _AxesConfig()
        self._split_sharp_edges = False
        self._sharp_edges_feature_angle = 30.0
        self._before_close_callback = None

        # Grab system flag for anti-aliasing
        try:
            self._multi_samples = int(os.environ.get('PYVISTA_MULTI_SAMPLES', 4))
        except ValueError:  # pragma: no cover
            self._multi_samples = 4

        # Grab system flag for auto-closing
        self._auto_close = os.environ.get('PYVISTA_AUTO_CLOSE', '').lower() != 'false'

        self._jupyter_backend = os.environ.get('PYVISTA_JUPYTER_BACKEND', 'trame')
        self._trame = _TrameConfig()

        self._multi_rendering_splitting_position = None
        self._volume_mapper = 'fixed_point' if os.name == 'nt' else 'smart'
        self._smooth_shading = False
        self._depth_peeling = _DepthPeelingConfig()
        self._silhouette = _SilhouetteConfig()
        self._slider_styles = _SliderConfig()
        self._return_cpos = True
        self._hidden_line_removal = False
        self._anti_aliasing = None
        self._enable_camera_orientation_widget = False

        self._lighting_params = _LightingConfig()
        self._interpolate_before_map = True
        self._opacity = 1.0

    @property
    def hidden_line_removal(self) -> bool:
        """Return or set hidden line removal.

        Wireframe geometry will be drawn using hidden line removal if
        the rendering engine supports it.

        See Also
        --------
        pyvista.Plotter.enable_hidden_line_removal

        Examples
        --------
        Enable hidden line removal.

        >>> import pyvista as pv
        >>> pv.global_theme.hidden_line_removal = True
        >>> pv.global_theme.hidden_line_removal
        True

        """
        return self._hidden_line_removal

    @hidden_line_removal.setter
    def hidden_line_removal(self, value: bool):
        self._hidden_line_removal = value

    @property
    def interpolate_before_map(self) -> bool:
        """Return or set whether to interpolate colors before mapping.

        If the ``interpolate_before_map`` is turned off, the color
        mapping occurs at polygon points and colors are interpolated,
        which is generally less accurate whereas if the
        ``interpolate_before_map`` is on (the default), then the scalars
        will be interpolated across the topology of the dataset which is
        more accurate.

        See also :ref:`interpolate_before_mapping_example`.

        Examples
        --------
        Enable hidden line removal.

        >>> import pyvista as pv

        Load a cylinder which has cells with a wide spread

        >>> cyl = pv.Cylinder(direction=(0, 0, 1), height=2).elevation()

        Common display argument to make sure all else is constant

        >>> dargs = dict(
        ...     scalars='Elevation', cmap='rainbow', show_edges=True
        ... )

        >>> p = pv.Plotter(shape=(1, 2))
        >>> _ = p.add_mesh(
        ...     cyl,
        ...     interpolate_before_map=False,
        ...     scalar_bar_args={'title': 'Elevation - interpolated'},
        ...     **dargs
        ... )
        >>> p.subplot(0, 1)
        >>> _ = p.add_mesh(
        ...     cyl,
        ...     interpolate_before_map=True,
        ...     scalar_bar_args={'title': 'Elevation - interpolated'},
        ...     **dargs
        ... )
        >>> p.link_views()
        >>> p.camera_position = [
        ...     (-1.67, -5.10, 2.06),
        ...     (0.0, 0.0, 0.0),
        ...     (0.00, 0.37, 0.93),
        ... ]
        >>> p.show()  # doctest: +SKIP

        """
        return self._interpolate_before_map

    @interpolate_before_map.setter
    def interpolate_before_map(self, value: bool):
        self._interpolate_before_map = value

    @property
    def opacity(self) -> float:
        """Return or set the opacity.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.opacity = 0.5

        """
        return self._opacity

    @opacity.setter
    def opacity(self, opacity: float):
        _check_range(opacity, (0, 1), 'opacity')
        self._opacity = float(opacity)

    @property
    def above_range_color(self) -> Color:
        """Return or set the default above range color.

        Examples
        --------
        Set the above range color to red.

        >>> import pyvista as pv
        >>> pv.global_theme.above_range_color = 'r'
        >>> pv.global_theme.above_range_color
        Color(name='red', hex='#ff0000ff', opacity=255)

        """
        return self._above_range_color

    @above_range_color.setter
    def above_range_color(self, value: ColorLike):
        self._above_range_color = Color(value)

    @property
    def below_range_color(self) -> Color:
        """Return or set the default below range color.

        Examples
        --------
        Set the below range color to blue.

        >>> import pyvista as pv
        >>> pv.global_theme.below_range_color = 'b'
        >>> pv.global_theme.below_range_color
        Color(name='blue', hex='#0000ffff', opacity=255)

        """
        return self._below_range_color

    @below_range_color.setter
    def below_range_color(self, value: ColorLike):
        self._below_range_color = Color(value)

    @property
    def return_cpos(self) -> bool:
        """Return or set the default behavior of returning the camera position.

        Examples
        --------
        Disable returning camera position by ``show`` and ``plot`` methods.

        >>> import pyvista as pv
        >>> pv.global_theme.return_cpos = False
        """
        return self._return_cpos

    @return_cpos.setter
    def return_cpos(self, value: bool):
        self._return_cpos = value

    @property
    def background(self) -> Color:
        """Return or set the default background color of pyvista plots.

        Examples
        --------
        Set the default global background of all plots to white.

        >>> import pyvista as pv
        >>> pv.global_theme.background = 'white'
        """
        return self._background

    @background.setter
    def background(self, new_background: ColorLike):
        self._background = Color(new_background)

    @property
    def jupyter_backend(self) -> str:
        """Return or set the jupyter notebook plotting backend.

        Jupyter backend to use when plotting.  Must be one of the
        following:

        * ``'ipyvtklink'`` : DEPRECATED. Render remotely and stream the
          resulting VTK images back to the client.  Supports all VTK
          methods, but suffers from lag due to remote rendering.
          Requires that a virtual framebuffer be set up when displaying
          on a headless server.  Must have ``ipyvtklink`` installed.

        * ``'panel'`` : Convert the VTK render window to a vtkjs
          object and then visualize that within jupyterlab. Supports
          most VTK objects.  Requires that a virtual framebuffer be
          set up when displaying on a headless server.  Must have
          ``panel`` installed.

        * ``'ipygany'`` : Convert all the meshes into ``ipygany``
          meshes and streams those to be rendered on the client side.
          Supports VTK meshes, but few others.  Aside from ``none``,
          this is the only method that does not require a virtual
          framebuffer.  Must have ``ipygany`` installed.

        * ``'pythreejs'`` : Convert all the meshes into ``pythreejs``
          meshes and streams those to be rendered on the client side.
          Aside from ``ipygany``, this is the only method that does
          not require a virtual framebuffer.  Must have ``pythreejs``
          installed.

        * ``'static'`` : Display a single static image within the
          JupyterLab environment.  Still requires that a virtual
          framebuffer be set up when displaying on a headless server,
          but does not require any additional modules to be installed.

        * ``'none'`` : Do not display any plots within jupyterlab,
          instead display using dedicated VTK render windows.  This
          will generate nothing on headless servers even with a
          virtual framebuffer.

        Examples
        --------
        Enable the pythreejs backend.

        >>> import pyvista as pv
        >>> pv.set_jupyter_backend('pythreejs')  # doctest:+SKIP

        Enable the ipygany backend.

        >>> pv.set_jupyter_backend('ipygany')  # doctest:+SKIP

        Enable the panel backend.

        >>> pv.set_jupyter_backend('panel')  # doctest:+SKIP

        Enable the ipyvtklink backend (DEPRECATED).

        >>> pv.set_jupyter_backend('ipyvtklink')  # doctest:+SKIP

        Just show static images.

        >>> pv.set_jupyter_backend('static')  # doctest:+SKIP

        Disable all plotting within JupyterLab and display using a
        standard desktop VTK render window.

        >>> pv.set_jupyter_backend(None)  # doctest:+SKIP

        """
        return self._jupyter_backend

    @jupyter_backend.setter
    def jupyter_backend(self, backend: 'str'):
        from pyvista.jupyter import _validate_jupyter_backend

        self._jupyter_backend = _validate_jupyter_backend(backend)

    @property
    def trame(self) -> _TrameConfig:
        """Return or set the default trame parameters."""
        return self._trame

    @trame.setter
    def trame(self, config: _TrameConfig):
        if not isinstance(config, _TrameConfig):
            raise TypeError('Configuration type must be `_TrameConfig`.')
        self._trame = config

    @property
    def auto_close(self) -> bool:
        """Automatically close the figures when finished plotting.

        .. DANGER::
           Set to ``False`` with extreme caution.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.auto_close = False

        """
        return self._auto_close

    @auto_close.setter
    def auto_close(self, value: bool):
        self._auto_close = value

    @property
    def full_screen(self) -> bool:
        """Return if figures are shown in full screen.

        Examples
        --------
        Set windows to be full screen by default.

        >>> import pyvista as pv
        >>> pv.global_theme.full_screen = True
        """
        return self._full_screen

    @full_screen.setter
    def full_screen(self, value: bool):
        self._full_screen = value

    @property
    def enable_camera_orientation_widget(self) -> bool:
        """Enable the camera orientation widget in all plotters.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.enable_camera_orientation_widget = True
        >>> pv.global_theme.enable_camera_orientation_widget
        True

        """
        return self._enable_camera_orientation_widget

    @enable_camera_orientation_widget.setter
    def enable_camera_orientation_widget(self, value: bool):
        self._enable_camera_orientation_widget = value

    @property
    def camera(self):
        """Return or set the default camera position.

        Examples
        --------
        Set both the position and view of the camera.

        >>> import pyvista as pv
        >>> pv.global_theme.camera = {
        ...     'position': [1, 1, 1],
        ...     'viewup': [0, 0, 1],
        ... }

        Set the default position of the camera.

        >>> pv.global_theme.camera['position'] = [1, 1, 1]

        Set the default view of the camera.

        >>> pv.global_theme.camera['viewup'] = [0, 0, 1]

        """
        return self._camera

    @camera.setter
    def camera(self, camera):
        if not isinstance(camera, dict):
            raise TypeError(f'Expected ``camera`` to be a dict, not {type(camera).__name__}.')

        if 'position' not in camera:
            raise KeyError('Expected the "position" key in the camera dict.')
        if 'viewup' not in camera:
            raise KeyError('Expected the "viewup" key in the camera dict.')

        self._camera = camera

    @property
    def notebook(self) -> Union[bool, None]:
        """Return or set the state of notebook plotting.

        Setting this to ``True`` always enables notebook plotting,
        while setting it to ``False`` disables plotting even when
        plotting within a jupyter notebook and plots externally.

        Examples
        --------
        Disable all jupyter notebook plotting.

        >>> import pyvista as pv
        >>> pv.global_theme.notebook = False

        """
        return self._notebook

    @notebook.setter
    def notebook(self, value: Union[bool, None]):
        self._notebook = value

    @property
    def window_size(self) -> List[int]:
        """Return or set the default render window size.

        Examples
        --------
        Set window size to ``[400, 400]``.

        >>> import pyvista as pv
        >>> pv.global_theme.window_size = [400, 400]

        """
        return self._window_size

    @window_size.setter
    def window_size(self, window_size: List[int]):
        if len(window_size) != 2:
            raise ValueError('Expected a length 2 iterable for ``window_size``.')

        # ensure positive size
        if window_size[0] < 0 or window_size[1] < 0:
            raise ValueError('Window size must be a positive value.')

        self._window_size = window_size

    @property
    def image_scale(self) -> int:
        """Return or set the default image scale factor."""
        return self._image_scale

    @image_scale.setter
    def image_scale(self, value: int):
        value = int(value)
        if value < 1:
            raise ValueError('Scale factor must be a positive integer.')
        self._image_scale = int(value)

    @property
    def font(self) -> _Font:
        """Return or set the default font size, family, and/or color.

        Examples
        --------
        Set the default font family to 'arial'.  Must be either
        'arial', 'courier', or 'times'.

        >>> import pyvista as pv
        >>> pv.global_theme.font.family = 'arial'

        Set the default font size to 20.

        >>> pv.global_theme.font.size = 20

        Set the default title size to 40.

        >>> pv.global_theme.font.title_size = 40

        Set the default label size to 10.

        >>> pv.global_theme.font.label_size = 10

        Set the default text color to 'grey'.

        >>> pv.global_theme.font.color = 'grey'

        String formatter used to format numerical data to '%.6e'.

        >>> pv.global_theme.font.fmt = '%.6e'

        """
        return self._font

    @font.setter
    def font(self, config: _Font):
        if not isinstance(config, _Font):
            raise TypeError('Configuration type must be `_Font`.')
        self._font = config

    @property
    def cmap(self):
        """Return or set the default colormap of pyvista.

        See available Matplotlib colormaps.  Only applicable for when
        displaying ``scalars``.  If ``colorcet`` or ``cmocean`` are
        installed, their colormaps can be specified by name.

        You can also specify a list of colors to override an existing
        colormap with a custom one.  For example, to create a three
        color colormap you might specify ``['green', 'red', 'blue']``

        Examples
        --------
        Set the default global colormap to 'jet'.

        >>> import pyvista as pv
        >>> pv.global_theme.cmap = 'jet'

        """
        return self._cmap

    @cmap.setter
    def cmap(self, cmap):
        out = get_cmap_safe(cmap)  # for validation
        if out is None:
            raise ValueError(f'Invalid color map {cmap}')
        self._cmap = cmap

    @property
    def color(self) -> Color:
        """Return or set the default color of meshes in pyvista.

        Used for meshes without ``scalars``.

        When setting, the value must be either a string, rgb list,
        or hex color string.  For example:

        * ``color='white'``
        * ``color='w'``
        * ``color=[1.0, 1.0, 1.0]``
        * ``color='#FFFFFF'``

        Examples
        --------
        Set the default mesh color to 'red'.

        >>> import pyvista as pv
        >>> pv.global_theme.color = 'red'

        """
        return self._color

    @color.setter
    def color(self, color: ColorLike):
        self._color = Color(color)

    @property
    def color_cycler(self):
        """Return or set the default color cycler used to color meshes.

        This color cycler is iterated over by each renderer to sequentially
        color datasets when displaying them through ``add_mesh``.

        When setting, the value must be either a list of color-like objects,
        or a cycler of color-like objects. If the value passed is a single
        string, it must be one of:

            * ``'default'`` - Use the default color cycler (matches matplotlib's default)
            * ``'matplotlib`` - Dynamically get matplotlib's current theme's color cycler.
            * ``'all'`` - Cycle through all of the available colors in ``pyvista.plotting.colors.hexcolors``

        Setting to ``None`` will disable the use of the color cycler.

        Examples
        --------
        Set the default color cycler to iterate through red, green, and blue.

        >>> import pyvista as pv
        >>> pv.global_theme.color_cycler = ['red', 'green', 'blue']

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(pv.Cone(center=(0, 0, 0)))  # red
        >>> _ = pl.add_mesh(pv.Cube(center=(1, 0, 0)))  # green
        >>> _ = pl.add_mesh(pv.Sphere(center=(1, 1, 0)))  # blue
        >>> _ = pl.add_mesh(pv.Cylinder(center=(0, 1, 0)))  # red again
        >>> pl.show()  # doctest: +SKIP

        """
        return self._color_cycler

    @color_cycler.setter
    def color_cycler(self, color_cycler):
        self._color_cycler = get_cycler(color_cycler)

    @property
    def nan_color(self) -> Color:
        """Return or set the default NaN color.

        This color is used to plot all NaN values.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.nan_color = 'darkgray'

        """
        return self._nan_color

    @nan_color.setter
    def nan_color(self, nan_color: ColorLike):
        self._nan_color = Color(nan_color)

    @property
    def edge_color(self) -> Color:
        """Return or set the default edge color.

        Examples
        --------
        Set the global edge color to 'blue'.

        >>> import pyvista as pv
        >>> pv.global_theme.edge_color = 'blue'

        """
        return self._edge_color

    @edge_color.setter
    def edge_color(self, edge_color: ColorLike):
        self._edge_color = Color(edge_color)

    @property
    def line_width(self) -> float:
        """Return or set the default line width.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.line_width = 2.0

        """
        return self._line_width

    @line_width.setter
    def line_width(self, line_width: float):
        self._line_width = float(line_width)

    @property
    def point_size(self) -> float:
        """Return or set the default point size.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.line_width = 10.0

        """
        return self._point_size

    @point_size.setter
    def point_size(self, point_size: float):
        self._point_size = float(point_size)

    @property
    def outline_color(self) -> Color:
        """Return or set the default outline color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.outline_color = 'white'

        """
        return self._outline_color

    @outline_color.setter
    def outline_color(self, outline_color: ColorLike):
        self._outline_color = Color(outline_color)

    @property
    def floor_color(self) -> Color:
        """Return or set the default floor color.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.floor_color = 'black'

        """
        return self._floor_color

    @floor_color.setter
    def floor_color(self, floor_color: ColorLike):
        self._floor_color = Color(floor_color)

    @property
    def colorbar_orientation(self) -> str:
        """Return or set the default colorbar orientation.

        Must be either ``'vertical'`` or ``'horizontal'``.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_orientation = 'horizontal'

        """
        return self._colorbar_orientation

    @colorbar_orientation.setter
    def colorbar_orientation(self, colorbar_orientation: str):
        if colorbar_orientation not in ['vertical', 'horizontal']:
            raise ValueError('Colorbar orientation must be either "vertical" or "horizontal"')
        self._colorbar_orientation = colorbar_orientation

    @property
    def colorbar_horizontal(self) -> _ColorbarConfig:
        """Return or set the default parameters of a horizontal colorbar.

        Examples
        --------
        Set the default horizontal colorbar width to 0.6.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_horizontal.width = 0.6

        Set the default horizontal colorbar height to 0.2.

        >>> pv.global_theme.colorbar_horizontal.height = 0.2

        """
        return self._colorbar_horizontal

    @colorbar_horizontal.setter
    def colorbar_horizontal(self, config: _ColorbarConfig):
        if not isinstance(config, _ColorbarConfig):
            raise TypeError('Configuration type must be `_ColorbarConfig`.')
        self._colorbar_horizontal = config

    @property
    def colorbar_vertical(self) -> _ColorbarConfig:
        """Return or set the default parameters of a vertical colorbar.

        Examples
        --------
        Set the default colorbar width to 0.45.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_vertical.width = 0.45

        Set the default colorbar height to 0.8.

        >>> import pyvista as pv
        >>> pv.global_theme.colorbar_vertical.height = 0.8

        """
        return self._colorbar_vertical

    @colorbar_vertical.setter
    def colorbar_vertical(self, config: _ColorbarConfig):
        if not isinstance(config, _ColorbarConfig):
            raise TypeError('Configuration type must be `_ColorbarConfig`.')
        self._colorbar_vertical = config

    @property
    def show_scalar_bar(self) -> bool:
        """Return or set the default color bar visibility.

        Examples
        --------
        Show the scalar bar by default when scalars are available.

        >>> import pyvista as pv
        >>> pv.global_theme.show_scalar_bar = True

        """
        return self._show_scalar_bar

    @show_scalar_bar.setter
    def show_scalar_bar(self, show_scalar_bar: bool):
        self._show_scalar_bar = bool(show_scalar_bar)

    @property
    def show_edges(self) -> bool:
        """Return or set the default edge visibility.

        Examples
        --------
        Show edges globally by default.

        >>> import pyvista as pv
        >>> pv.global_theme.show_edges = True

        """
        return self._show_edges

    @show_edges.setter
    def show_edges(self, show_edges: bool):
        self._show_edges = bool(show_edges)

    @property
    def show_vertices(self) -> bool:
        """Return or set the default vertex visibility.

        Examples
        --------
        Show vertices globally by default.

        >>> import pyvista as pv
        >>> pv.global_theme.show_vertices = True

        """
        return self._show_vertices

    @show_vertices.setter
    def show_vertices(self, show_vertices: bool):
        self._show_vertices = bool(show_vertices)

    @property
    def lighting(self) -> bool:
        """Return or set the default ``lighting``.

        Examples
        --------
        Disable lighting globally.

        >>> import pyvista as pv
        >>> pv.global_theme.lighting = False
        """
        return self._lighting

    @lighting.setter
    def lighting(self, lighting: bool):
        self._lighting = lighting

    @property
    def interactive(self) -> bool:
        """Return or set the default ``interactive`` parameter.

        Examples
        --------
        Make all plots non-interactive globally.

        >>> import pyvista as pv
        >>> pv.global_theme.interactive = False
        """
        return self._interactive

    @interactive.setter
    def interactive(self, interactive: bool):
        self._interactive = bool(interactive)

    @property
    def render_points_as_spheres(self) -> bool:
        """Return or set the default ``render_points_as_spheres`` parameter.

        Examples
        --------
        Render points as spheres by default globally.

        >>> import pyvista as pv
        >>> pv.global_theme.render_points_as_spheres = True
        """
        return self._render_points_as_spheres

    @render_points_as_spheres.setter
    def render_points_as_spheres(self, render_points_as_spheres: bool):
        self._render_points_as_spheres = bool(render_points_as_spheres)

    @property
    def render_lines_as_tubes(self) -> bool:
        """Return or set the default ``render_lines_as_tubes`` parameter.

        Examples
        --------
        Render points as spheres by default globally.

        >>> import pyvista as pv
        >>> pv.global_theme.render_lines_as_tubes = True
        """
        return self._render_lines_as_tubes

    @render_lines_as_tubes.setter
    def render_lines_as_tubes(self, render_lines_as_tubes: bool):
        self._render_lines_as_tubes = bool(render_lines_as_tubes)

    @property
    def transparent_background(self) -> bool:
        """Return or set the default ``transparent_background`` parameter.

        Examples
        --------
        Set transparent_background globally to ``True``.

        >>> import pyvista as pv
        >>> pv.global_theme.transparent_background = True

        """
        return self._transparent_background

    @transparent_background.setter
    def transparent_background(self, transparent_background: bool):
        self._transparent_background = transparent_background

    @property
    def title(self) -> str:
        """Return or set the default ``title`` parameter.

        This is the VTK render window title.

        Examples
        --------
        Set title globally to 'plot'.

        >>> import pyvista as pv
        >>> pv.global_theme.title = 'plot'

        """
        return self._title

    @title.setter
    def title(self, title: str):
        self._title = title

    @property
    def anti_aliasing(self) -> Optional[str]:
        """Enable or disable anti-aliasing.

        Should be either ``"ssaa"``, ``"msaa"``, ``"fxaa"``, or ``None``.

        Examples
        --------
        Use super-sampling anti-aliasing in the global theme.

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = 'ssaa'
        >>> pv.global_theme.anti_aliasing
        'ssaa'

        Disable anti-aliasing in the global theme.

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = None

        See :ref:`anti_aliasing_example` for more information regarding
        anti-aliasing.

        """
        return self._anti_aliasing

    @anti_aliasing.setter
    def anti_aliasing(self, anti_aliasing: Union[str, None]):
        if isinstance(anti_aliasing, bool):
            # Deprecated on v0.37.0, estimated removal on v0.40.0
            warnings.warn(
                '`anti_aliasing` is now a string or None and must be either "ssaa", '
                '"msaa", "fxaa", or None',
                PyVistaDeprecationWarning,
            )
            anti_aliasing = 'fxaa' if anti_aliasing else None

        if isinstance(anti_aliasing, str):
            if anti_aliasing not in ['ssaa', 'msaa', 'fxaa']:
                raise ValueError('anti_aliasing must be either "ssaa", "msaa", or "fxaa"')
        elif anti_aliasing is not None:
            raise TypeError('anti_aliasing must be either "ssaa", "msaa", "fxaa", or None')

        self._anti_aliasing = anti_aliasing

    @property
    def antialiasing(self):
        """Enable or disable anti-aliasing.

        .. deprecated:: 0.37.0
           Deprecated in favor of :attr:`anti_aliasing <DefaultTheme.anti_aliasing>`.
        """
        # Recommended removing at pyvista==0.40.0
        warnings.warn(
            'antialising is deprecated.  Please use `anti_aliasing` instead.',
            PyVistaDeprecationWarning,
        )
        return self.anti_aliasing

    @antialiasing.setter
    def antialiasing(self, value):  # pragma: no cover
        # Recommended removing at pyvista==0.40.0
        warnings.warn(
            'antialising is deprecated.  Please use `anti_aliasing` instead.',
            PyVistaDeprecationWarning,
        )
        self.anti_aliasing = value

    @property
    def multi_samples(self) -> int:
        """Return or set the default ``multi_samples`` parameter.

        Set the number of multisamples to used with hardware anti_aliasing. This
        is only used when :attr:`anti_aliasing <DefaultTheme.anti_aliasing>` is
        set to ``"msaa"``.

        Examples
        --------
        Set the default number of multisamples to 2 and enable ``"msaa"``

        >>> import pyvista as pv
        >>> pv.global_theme.anti_aliasing = 'msaa'
        >>> pv.global_theme.multi_samples = 2

        """
        return self._multi_samples

    @multi_samples.setter
    def multi_samples(self, multi_samples: int):
        self._multi_samples = int(multi_samples)

    @property
    def multi_rendering_splitting_position(self) -> float:
        """Return or set the default ``multi_rendering_splitting_position`` parameter.

        Examples
        --------
        Set multi_rendering_splitting_position globally to 0.5 (the
        middle of the window).

        >>> import pyvista as pv
        >>> pv.global_theme.multi_rendering_splitting_position = 0.5

        """
        return self._multi_rendering_splitting_position

    @multi_rendering_splitting_position.setter
    def multi_rendering_splitting_position(self, multi_rendering_splitting_position: float):
        self._multi_rendering_splitting_position = multi_rendering_splitting_position

    @property
    def volume_mapper(self) -> str:
        """Return or set the default ``volume_mapper`` parameter.

        Must be one of the following strings, which are mapped to the
        following VTK volume mappers.

        * ``'fixed_point'`` : ``vtk.vtkFixedPointVolumeRayCastMapper``
        * ``'gpu'`` : ``vtk.vtkGPUVolumeRayCastMapper``
        * ``'open_gl'`` : ``vtk.vtkOpenGLGPUVolumeRayCastMapper``
        * ``'smart'`` : ``vtk.vtkSmartVolumeMapper``

        Examples
        --------
        Set default volume mapper globally to 'gpu'.

        >>> import pyvista as pv
        >>> pv.global_theme.volume_mapper = 'gpu'

        """
        return self._volume_mapper

    @volume_mapper.setter
    def volume_mapper(self, mapper: str):
        mappers = ['fixed_point', 'gpu', 'open_gl', 'smart']
        if mapper not in mappers:
            raise ValueError(
                f"Mapper ({mapper}) unknown. Available volume mappers "
                f"include:\n {', '.join(mappers)}"
            )

        self._volume_mapper = mapper

    @property
    def smooth_shading(self) -> bool:
        """Return or set the default ``smooth_shading`` parameter.

        Examples
        --------
        Set the global smooth_shading parameter default to ``True``.

        >>> import pyvista as pv
        >>> pv.global_theme.smooth_shading = True

        """
        return self._smooth_shading

    @smooth_shading.setter
    def smooth_shading(self, smooth_shading: bool):
        self._smooth_shading = bool(smooth_shading)

    @property
    def depth_peeling(self) -> _DepthPeelingConfig:
        """Return or set the default depth peeling parameters.

        Examples
        --------
        Set the global depth_peeling parameter default to be enabled
        with 8 peels.

        >>> import pyvista as pv
        >>> pv.global_theme.depth_peeling.number_of_peels = 8
        >>> pv.global_theme.depth_peeling.occlusion_ratio = 0.0
        >>> pv.global_theme.depth_peeling.enabled = True

        """
        return self._depth_peeling

    @depth_peeling.setter
    def depth_peeling(self, config: _DepthPeelingConfig):
        if not isinstance(config, _DepthPeelingConfig):
            raise TypeError('Configuration type must be `_DepthPeelingConfig`.')
        self._depth_peeling = config

    @property
    def silhouette(self) -> _SilhouetteConfig:
        """Return or set the default ``silhouette`` configuration.

        Examples
        --------
        Set parameters of the silhouette.

        >>> import pyvista as pv
        >>> pv.global_theme.silhouette.color = 'grey'
        >>> pv.global_theme.silhouette.line_width = 2.0
        >>> pv.global_theme.silhouette.feature_angle = 20

        """
        return self._silhouette

    @silhouette.setter
    def silhouette(self, config: _SilhouetteConfig):
        if not isinstance(config, _SilhouetteConfig):
            raise TypeError('Configuration type must be `_SilhouetteConfig`')
        self._silhouette = config

    @property
    def slider_styles(self) -> _SliderConfig:
        """Return the default slider style configurations."""
        return self._slider_styles

    @slider_styles.setter
    def slider_styles(self, config: _SliderConfig):
        if not isinstance(config, _SliderConfig):
            raise TypeError('Configuration type must be `_SliderConfig`.')
        self._slider_styles = config

    @property
    def axes(self) -> _AxesConfig:
        """Return or set the default ``axes`` configuration.

        Examples
        --------
        Set the x axis color to black.

        >>> import pyvista as pv
        >>> pv.global_theme.axes.x_color = 'black'

        Show axes by default.

        >>> pv.global_theme.axes.show = True

        Use the ``vtk.vtkCubeAxesActor``.

        >>> pv.global_theme.axes.box = True

        """
        return self._axes

    @axes.setter
    def axes(self, config: _AxesConfig):
        if not isinstance(config, _AxesConfig):
            raise TypeError('Configuration type must be `_AxesConfig`.')
        self._axes = config

    @property
    def before_close_callback(self) -> Callable[[Plotter], None]:
        """Return the default before_close_callback function for Plotter."""
        return self._before_close_callback

    @before_close_callback.setter
    def before_close_callback(self, value: Callable[[Plotter], None]):
        self._before_close_callback = value

    def restore_defaults(self):
        """Restore the theme defaults.

        Examples
        --------
        >>> import pyvista as pv
        >>> pv.global_theme.restore_defaults()

        """
        self.__init__()

    def __repr__(self):
        """User friendly representation of the current theme."""
        txt = [f'{self.name.capitalize()} Theme']
        txt.append('-' * len(txt[0]))
        parm = {
            'Background': 'background',
            'Jupyter backend': 'jupyter_backend',
            'Full screen': 'full_screen',
            'Window size': 'window_size',
            'Camera': 'camera',
            'Notebook': 'notebook',
            'Font': 'font',
            'Auto close': 'auto_close',
            'Colormap': 'cmap',
            'Color': 'color',
            'Color Cycler': 'color_cycler',
            'NaN color': 'nan_color',
            'Edge color': 'edge_color',
            'Outline color': 'outline_color',
            'Floor color': 'floor_color',
            'Colorbar orientation': 'colorbar_orientation',
            'Colorbar - horizontal': 'colorbar_horizontal',
            'Colorbar - vertical': 'colorbar_vertical',
            'Show scalar bar': 'show_scalar_bar',
            'Show edges': 'show_edges',
            'Lighting': 'lighting',
            'Interactive': 'interactive',
            'Render points as spheres': 'render_points_as_spheres',
            'Transparent Background': 'transparent_background',
            'Title': 'title',
            'Axes': 'axes',
            'Multi-samples': 'multi_samples',
            'Multi-renderer Split Pos': 'multi_rendering_splitting_position',
            'Volume mapper': 'volume_mapper',
            'Smooth shading': 'smooth_shading',
            'Depth peeling': 'depth_peeling',
            'Silhouette': 'silhouette',
            'Slider Styles': 'slider_styles',
            'Return Camera Position': 'return_cpos',
            'Hidden Line Removal': 'hidden_line_removal',
            'Anti-Aliasing': '_anti_aliasing',
            'Split sharp edges': '_split_sharp_edges',
            'Sharp edges feat. angle': '_sharp_edges_feature_angle',
            'Before close callback': '_before_close_callback',
        }
        for name, attr in parm.items():
            setting = getattr(self, attr)
            txt.append(f'{name:<25}: {setting}')

        return '\n'.join(txt)

    @property
    def name(self) -> str:
        """Return or set the name of the theme."""
        return self._name

    @name.setter
    def name(self, name: str):
        self._name = name

    def load_theme(self, theme):
        """Overwrite the current theme with a theme.

        Parameters
        ----------
        theme : pyvista.themes.DefaultTheme
            Theme to use to overwrite this theme.

        Examples
        --------
        Create a custom theme from the default theme and load it into
        the global theme of pyvista.

        >>> import pyvista as pv
        >>> from pyvista.themes import DefaultTheme
        >>> my_theme = DefaultTheme()
        >>> my_theme.font.size = 20
        >>> my_theme.font.title_size = 40
        >>> my_theme.cmap = 'jet'
        >>> pv.global_theme.load_theme(my_theme)
        >>> pv.global_theme.font.size
        20

        Create a custom theme from the dark theme and load it into
        pyvista.

        >>> from pyvista.themes import DarkTheme
        >>> my_theme = DarkTheme()
        >>> my_theme.show_edges = True
        >>> pv.global_theme.load_theme(my_theme)
        >>> pv.global_theme.show_edges
        True

        """
        if isinstance(theme, str):
            theme = load_theme(theme)

        if not isinstance(theme, DefaultTheme):
            raise TypeError(
                '``theme`` must be a pyvista theme like ``pyvista.themes.DefaultTheme``.'
            )

        for attr_name in theme.__slots__:
            setattr(self, attr_name, getattr(theme, attr_name))

    def save(self, filename):
        """Serialize this theme to a json file.

        ``before_close_callback`` is non-serializable and is omitted.

        Parameters
        ----------
        filename : str
            Path to save the theme to.  Should end in ``'.json'``.

        Examples
        --------
        Export and then load back in a theme.

        >>> import pyvista as pv
        >>> theme = pv.themes.DefaultTheme()
        >>> theme.background = 'white'
        >>> theme.save('my_theme.json')  # doctest:+SKIP
        >>> loaded_theme = pv.load_theme('my_theme.json')  # doctest:+SKIP

        """
        data = self.to_dict()
        # functions are not serializable
        del data["before_close_callback"]
        with open(filename, 'w') as f:
            json.dump(data, f)

    @property
    def split_sharp_edges(self) -> bool:
        """Set or return splitting sharp edges.

        See :ref:`shading_example` for an example showing split sharp edges.

        Examples
        --------
        Enable the splitting of sharp edges globally.

        >>> import pyvista as pv
        >>> pv.global_theme.split_sharp_edges = True
        >>> pv.global_theme.split_sharp_edges
        True

        Disable the splitting of sharp edges globally.

        >>> import pyvista as pv
        >>> pv.global_theme.split_sharp_edges = False
        >>> pv.global_theme.split_sharp_edges
        False

        """
        return self._split_sharp_edges

    @split_sharp_edges.setter
    def split_sharp_edges(self, value: bool):
        self._split_sharp_edges = value

    @property
    def sharp_edges_feature_angle(self) -> float:
        """Set or return the angle of the sharp edges feature angle.

        See :ref:`shading_example` for an example showing split sharp edges.

        Examples
        --------
        Change the sharp edges feature angle to 45 degrees.

        >>> import pyvista as pv
        >>> pv.global_theme.sharp_edges_feature_angle = 45.0
        >>> pv.global_theme.sharp_edges_feature_angle
        45.0

        """
        return self._sharp_edges_feature_angle

    @sharp_edges_feature_angle.setter
    def sharp_edges_feature_angle(self, value: float):
        self._sharp_edges_feature_angle = float(value)

    @property
    def lighting_params(self) -> _LightingConfig:
        """Return or set the default lighting configuration."""
        return self._lighting_params

    @lighting_params.setter
    def lighting_params(self, config: _LightingConfig):
        if not isinstance(config, _LightingConfig):
            raise TypeError('Configuration type must be `_LightingConfig`.')
        self._lighting_params = config


class DarkTheme(DefaultTheme):
    """Dark mode theme.

    Black background, "viridis" colormap, tan meshes, white (hidden) edges.

    Examples
    --------
    Make the dark theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.DarkTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('dark')

    """

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.name = 'dark'
        self.background = 'black'
        self.cmap = 'viridis'
        self.font.color = 'white'
        self.show_edges = False
        self.color = 'tan'
        self.outline_color = 'white'
        self.edge_color = 'white'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'seagreen'
        self.axes.z_color = 'blue'


class ParaViewTheme(DefaultTheme):
    """A paraview-like theme.

    Examples
    --------
    Make the paraview-like theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.ParaViewTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('paraview')

    """

    def __init__(self):
        """Initialize theme."""
        super().__init__()
        self.name = 'paraview'
        self.background = 'paraview'
        self.cmap = 'coolwarm'
        self.font.family = 'arial'
        self.font.label_size = 16
        self.font.color = 'white'
        self.show_edges = False
        self.color = 'white'
        self.outline_color = 'white'
        self.edge_color = 'black'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'gold'
        self.axes.z_color = 'green'


class DocumentTheme(DefaultTheme):
    """A document theme well suited for papers and presentations.

    This theme uses:

    * A white background
    * Black fonts
    * The "viridis" colormap
    * disables edges for surface plots
    * Hidden edge removal

    Best used for presentations, papers, etc.

    Examples
    --------
    Make the document theme the global default.

    >>> import pyvista as pv
    >>> from pyvista import themes
    >>> pv.set_plot_theme(themes.DocumentTheme())

    Alternatively, set via a string.

    >>> pv.set_plot_theme('document')

    """

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.name = 'document'
        self.background = 'white'
        self.cmap = 'viridis'
        self.font.size = 18
        self.font.title_size = 18
        self.font.label_size = 18
        self.font.color = 'black'
        self.show_edges = False
        self.color = 'tan'
        self.outline_color = 'black'
        self.edge_color = 'black'
        self.axes.x_color = 'tomato'
        self.axes.y_color = 'seagreen'
        self.axes.z_color = 'blue'


class DocumentProTheme(DocumentTheme):
    """A more professional document theme.

    This theme extends the base document theme with:

    * Default color cycling
    * Rendering points as spheres
    * MSAA anti aliassing
    * Depth peeling

    """

    def __init__(self):
        """Initialize the theme."""
        super().__init__()
        self.color_cycler = get_cycler('default')
        self.render_points_as_spheres = True
        self.anti_aliasing = 'msaa'  # or 'ssaa'?
        self.multi_samples = 2
        self.depth_peeling.number_of_peels = 4
        self.depth_peeling.occlusion_ratio = 0.0
        self.depth_peeling.enabled = True


class _TestingTheme(DefaultTheme):
    """Low resolution testing theme for ``pytest``.

    Necessary for image regression.  Xvfb doesn't support
    multi-sampling, it's disabled for consistency between desktops and
    remote testing.

    Also disables ``return_cpos`` to make it easier for us to write
    examples without returning camera positions.

    """

    def __init__(self):
        super().__init__()
        self.name = 'testing'
        self.multi_samples = 1
        self.window_size = [400, 400]
        self.axes.show = False
        self.return_cpos = False


class _NATIVE_THEMES(Enum):
    """Global built-in themes available to PyVista."""

    paraview = ParaViewTheme
    document = DocumentTheme
    document_pro = DocumentProTheme
    dark = DarkTheme
    default = DefaultTheme
    testing = _TestingTheme


# pyvista/core/__init__.py

"""Core routines."""

from .dataset import DataSet, DataObject
from .composite import MultiBlock
from .datasetattributes import DataSetAttributes
from .filters import (
    CompositeFilters,
    DataSetFilters,
    PolyDataFilters,
    UnstructuredGridFilters,
    UniformGridFilters,
)
from .grid import Grid, RectilinearGrid, UniformGrid
from .objects import Table
from .texture import Texture
from .pointset import (
    PointGrid,
    PointSet,
    PolyData,
    StructuredGrid,
    UnstructuredGrid,
    ExplicitStructuredGrid,
)
from .pyvista_ndarray import pyvista_ndarray
from .celltype import CellType
from .cell import Cell


# pyvista/core/cell.py

"""Contains the pyvista.Cell class."""
from __future__ import annotations

from typing import List, Tuple

import numpy as np

import pyvista
from pyvista import _vtk

from .celltype import CellType
from .dataset import DataObject


class Cell(_vtk.vtkGenericCell, DataObject):
    """Wrapping of vtkCell.

    This class provides the capability to access a given cell topology and can
    be useful when walking through a cell's individual faces or investigating
    cell properties.

    Parameters
    ----------
    vtk_cell : vtk.vtkCell, optional
        The vtk object to wrap as Cell, that must be of ``vtk.vtkCell`` type.

    cell_type : int, optional
        VTK cell type. Determined from ``vtk_cell`` if not input.

    deep : bool, default: False
        Perform a deep copy of the original cell.

    Notes
    -----
    Accessing individual cells from a :class:`pyvista.DataSet` using this class
    will be much slower than accessing bulk data from the
    :attr:`pyvista.PolyData.faces` or :attr:`pyvista.UnstructuredGrid.cells` attributes.

    Also note that the cell object is a deep copy of the original cell and
    is unassociated with the original cell. Changing any data of
    that cell (for example, :attr:`pyvista.Cell.points`) will not change the original dataset.

    Examples
    --------
    Get the 0-th cell from a :class:`pyvista.PolyData`.

    >>> import pyvista
    >>> mesh = pyvista.Sphere()
    >>> cell = mesh.get_cell(0)
    >>> cell  # doctest: +SKIP
    Cell (0x7fa760075a10)
      Type:       <CellType.TRIANGLE: 5>
      Linear:     True
      Dimension:  2
      N Points:	  3
      N Faces:    0
      N Edges:    3
      X Bounds:   -5.406e-02, -5.551e-17
      Y Bounds:	  0.000e+00, 1.124e-02
      Z Bounds:   -5.000e-01, -4.971e-01

    Get the 0-th cell from a :class:`pyvista.UnstructuredGrid`.

    >>> from pyvista import examples
    >>> mesh = examples.load_hexbeam()
    >>> cell = mesh.get_cell(0)
    >>> cell  # doctest: +SKIP
    Cell (0x7fdc71a3c210)
      Type:       <CellType.HEXAHEDRON: 12>
      Linear:     True
      Dimension:  3
      N Points:   8
      N Faces:    6
      N Edges:    12
      X Bounds:   0.000e+00, 5.000e-01
      Y Bounds:   0.000e+00, 5.000e-01
      Z Bounds:   0.000e+00, 5.000e-01

    """

    def __init__(self, vtk_cell=None, cell_type=None, deep=False):
        """Initialize the cell."""
        super().__init__()
        if vtk_cell is not None:
            if not isinstance(vtk_cell, _vtk.vtkCell):
                raise TypeError(f'`vtk_cell` must be a vtkCell, not {type(vtk_cell)}')
            # cell type must be set first before deep or shallow copy
            if cell_type is None:
                self.SetCellType(vtk_cell.GetCellType())
            else:
                self.SetCellType(cell_type)

            if deep:
                self.DeepCopy(vtk_cell)
            else:
                self.ShallowCopy(vtk_cell)

    @property
    def type(self) -> CellType:
        """Get the cell type from the enum :class:`pyvista.CellType`.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).type
        <CellType.TRIANGLE: 5>
        """
        return CellType(self.GetCellType())

    @property
    def is_linear(self) -> bool:
        """Return if the cell is linear.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).is_linear
        True

        """
        return bool(self.IsLinear())

    def plot(self, **kwargs):
        """Plot this cell.

        Parameters
        ----------
        **kwargs : dict, optional
            See :func:`pyvista.plot` for a description of the optional keyword
            arguments.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_hexbeam()
        >>> cell = mesh.get_cell(0)
        >>> cell.plot()

        """
        self.cast_to_unstructured_grid().plot(**kwargs)

    def cast_to_unstructured_grid(self) -> pyvista.UnstructuredGrid:
        """Cast this cell to an unstructured grid.

        Returns
        -------
        pyvista.UnstructuredGrid
            This cell cast to a :class:`pyvista.UnstructuredGrid`.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_hexbeam()
        >>> cell = mesh.get_cell(0)
        >>> grid = cell.cast_to_unstructured_grid()
        >>> grid  # doctest: +SKIP
        UnstructuredGrid (0x7f9383619540)
          N Cells:      1
          N Points:     8
          X Bounds:     0.000e+00, 5.000e-01
          Y Bounds:     0.000e+00, 5.000e-01
          Z Bounds:     0.000e+00, 5.000e-01
          N Arrays:     0

        """
        return pyvista.UnstructuredGrid(
            [len(self.point_ids)] + list(range(len(self.point_ids))),
            [int(self.type)],
            self.points.copy(),
        )

    @property
    def dimension(self) -> int:
        """Return the cell dimension.

        This returns the dimensionality of the cell. For example, 1 for an edge,
        2 for a triangle, and 3 for a tetrahedron.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).dimension
        2
        """
        return self.GetCellDimension()

    @property
    def n_points(self) -> int:
        """Get the number of points composing the cell.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).n_points
        3
        """
        return self.GetNumberOfPoints()

    @property
    def n_faces(self) -> int:
        """Get the number of faces composing the cell.

        Examples
        --------
        >>> from pyvista.examples.cells import Tetrahedron
        >>> mesh = Tetrahedron()
        >>> mesh.get_cell(0).n_faces
        4
        """
        return self.GetNumberOfFaces()

    @property
    def n_edges(self) -> int:
        """Get the number of edges composing the cell.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).n_edges
        3
        """
        return self.GetNumberOfEdges()

    @property
    def point_ids(self) -> List[int]:
        """Get the point IDs composing the cell.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).point_ids
        [2, 30, 0]
        """
        point_ids = self.GetPointIds()
        return [point_ids.GetId(i) for i in range(point_ids.GetNumberOfIds())]

    @property
    def points(self) -> np.ndarray:
        """Get the point coordinates of the cell.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).points
        array([[-5.40595092e-02,  0.00000000e+00, -4.97068971e-01],
               [-5.28781787e-02,  1.12396041e-02, -4.97068971e-01],
               [-5.55111512e-17,  0.00000000e+00, -5.00000000e-01]])
        """
        return _vtk.vtk_to_numpy(self.GetPoints().GetData())

    def get_edge(self, index: int) -> Cell:
        """Get the i-th edge composing the cell.

        Parameters
        ----------
        index : int
            Edge ID.

        Returns
        -------
        pyvista.Cell
            Edge given by ``index``.

        Examples
        --------
        Extract a single edge from a face and output the IDs of the edge
        points.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> cell = mesh.get_cell(0)
        >>> edge = cell.get_edge(0)
        >>> edge.point_ids
        [2, 30]

        """
        if index + 1 > self.n_edges:
            raise IndexError(f'Invalid index {index} for a cell with {self.n_edges} edges.')

        # must deep copy here as multiple sequental calls to GetEdge overwrite
        # the underlying pointer
        return Cell(self.GetEdge(index), deep=True)

    @property
    def edges(self) -> List[Cell]:
        """Return a list of edges composing the cell.

        >>> from pyvista.examples.cells import Hexahedron
        >>> mesh = Hexahedron()
        >>> cell = mesh.get_cell(0)
        >>> edges = cell.edges
        >>> len(edges)
        12

        """
        return [self.get_edge(i) for i in range(self.n_edges)]

    @property
    def faces(self) -> List[Cell]:
        """Return a list of faces composing the cell.

        >>> from pyvista.examples.cells import Tetrahedron
        >>> mesh = Tetrahedron()
        >>> cell = mesh.get_cell(0)
        >>> faces = cell.faces
        >>> len(faces)
        4

        """
        return [self.get_face(i) for i in range(self.n_faces)]

    def get_face(self, index: int) -> Cell:
        """Get the i-th face composing the cell.

        Parameters
        ----------
        index : int
            Face ID.

        Returns
        -------
        pyvista.Cell
            Face given by ``index``.

        Examples
        --------
        Return the face IDs composing the first face of an example tetrahedron.

        >>> from pyvista.examples.cells import Tetrahedron
        >>> mesh = Tetrahedron()
        >>> cell = mesh.get_cell(0)
        >>> face = cell.get_face(0)
        >>> face.point_ids
        [0, 1, 3]

        """
        # must deep copy here as sequental calls overwrite the underlying pointer
        if index + 1 > self.n_faces:
            raise IndexError(f'Invalid index {index} for a cell with {self.n_faces} faces.')

        # must deep copy here as multiple sequental calls to GetFace overwrite
        # the underlying pointer
        cell = self.GetFace(index)
        return Cell(cell, deep=True, cell_type=cell.GetCellType())

    @property
    def bounds(self) -> Tuple[float, float, float, float, float, float]:
        """Get the cell bounds in ``[xmin, xmax, ymin, ymax, zmin, zmax]``.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.get_cell(0).bounds
        (-0.05405950918793678, -5.551115123125783e-17, 0.0, 0.011239604093134403, -0.5, -0.49706897139549255)
        """
        return self.GetBounds()

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = []
        attrs.append(("Type", repr(self.type), "{}" * len(repr(self.type))))
        attrs.append(("Linear", self.is_linear, "{}"))
        attrs.append(("Dimension", self.dimension, "{}"))
        attrs.append(("N Points", self.n_points, "{}"))
        attrs.append(("N Faces", self.n_faces, "{}"))
        attrs.append(("N Edges", self.n_edges, "{}"))
        bds = self.bounds
        fmt = f"{pyvista.FLOAT_FORMAT}, {pyvista.FLOAT_FORMAT}"
        attrs.append(("X Bounds", (bds[0], bds[1]), fmt))
        attrs.append(("Y Bounds", (bds[2], bds[3]), fmt))
        attrs.append(("Z Bounds", (bds[4], bds[5]), fmt))

        return attrs

    def __repr__(self) -> str:
        """Return the object representation."""
        return self.head(display=False, html=False)

    def __str__(self) -> str:
        """Return the object string representation."""
        return self.head(display=False, html=False)

    def copy(self, deep=True) -> Cell:
        """Return a copy of the cell.

        Parameters
        ----------
        deep : bool, optional
            When ``True`` makes a full copy of the cell.  When ``False``,
            performs a shallow copy where the new cell still references the
            original cell.

        Returns
        -------
        pyvista.Cell
            Deep or shallow copy of the cell.

        Examples
        --------
        Create a deep copy of the cell and demonstrate it is deep.

        >>> from pyvista.examples.cells import Tetrahedron
        >>> mesh = Tetrahedron()
        >>> cell = mesh.get_cell(0)
        >>> deep_cell = cell.copy(deep=True)
        >>> deep_cell.points[:] = 0
        >>> cell != deep_cell
        True

        Create a shallow copy of the cell and demonstrate it is shallow.

        >>> shallow_cell = cell.copy(deep=False)
        >>> shallow_cell.points[:] = 0
        >>> cell == shallow_cell
        True

        """
        return type(self)(self, deep=deep)


# pyvista/core/celltype.py

"""Define types of cells."""
from enum import IntEnum

from pyvista import _vtk


class CellType(IntEnum):
    """Define types of cells.

    Notes
    -----
    See `vtkCellType.h
    <https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html>`_ for all
    cell types.

    Examples
    --------
    Create a single cube. Notice how the cell type is defined using the
    ``CellType``.

    >>> import numpy as np
    >>> from pyvista import CellType
    >>> import pyvista
    >>> cells = np.array([8, 0, 1, 2, 3, 4, 5, 6, 7])
    >>> cell_type = np.array([CellType.HEXAHEDRON], np.int8)
    >>> points = np.array(
    ...     [
    ...         [0, 0, 0],
    ...         [1, 0, 0],
    ...         [1, 1, 0],
    ...         [0, 1, 0],
    ...         [0, 0, 1],
    ...         [1, 0, 1],
    ...         [1, 1, 1],
    ...         [0, 1, 1],
    ...     ],
    ...     dtype=np.float32,
    ... )
    >>> grid = pyvista.UnstructuredGrid(cells, cell_type, points)
    >>> grid
    UnstructuredGrid (...)
      N Cells:    1
      N Points:   8
      X Bounds:   0.000e+00, 1.000e+00
      Y Bounds:   0.000e+00, 1.000e+00
      Z Bounds:   0.000e+00, 1.000e+00
      N Arrays:   0

    """

    # Linear cells
    EMPTY_CELL = _vtk.VTK_EMPTY_CELL
    VERTEX = _vtk.VTK_VERTEX
    POLY_VERTEX = _vtk.VTK_POLY_VERTEX
    LINE = _vtk.VTK_LINE
    POLY_LINE = _vtk.VTK_POLY_LINE
    TRIANGLE = _vtk.VTK_TRIANGLE
    TRIANGLE_STRIP = _vtk.VTK_TRIANGLE_STRIP
    POLYGON = _vtk.VTK_POLYGON
    PIXEL = _vtk.VTK_PIXEL
    QUAD = _vtk.VTK_QUAD
    TETRA = _vtk.VTK_TETRA
    VOXEL = _vtk.VTK_VOXEL
    HEXAHEDRON = _vtk.VTK_HEXAHEDRON
    WEDGE = _vtk.VTK_WEDGE
    PYRAMID = _vtk.VTK_PYRAMID
    PENTAGONAL_PRISM = _vtk.VTK_PENTAGONAL_PRISM
    HEXAGONAL_PRISM = _vtk.VTK_HEXAGONAL_PRISM

    # Quadratic, isoparametric cells
    QUADRATIC_EDGE = _vtk.VTK_QUADRATIC_EDGE
    QUADRATIC_TRIANGLE = _vtk.VTK_QUADRATIC_TRIANGLE
    QUADRATIC_QUAD = _vtk.VTK_QUADRATIC_QUAD
    QUADRATIC_POLYGON = _vtk.VTK_QUADRATIC_POLYGON
    QUADRATIC_TETRA = _vtk.VTK_QUADRATIC_TETRA
    QUADRATIC_HEXAHEDRON = _vtk.VTK_QUADRATIC_HEXAHEDRON
    QUADRATIC_WEDGE = _vtk.VTK_QUADRATIC_WEDGE
    QUADRATIC_PYRAMID = _vtk.VTK_QUADRATIC_PYRAMID
    BIQUADRATIC_QUAD = _vtk.VTK_BIQUADRATIC_QUAD
    TRIQUADRATIC_HEXAHEDRON = _vtk.VTK_TRIQUADRATIC_HEXAHEDRON
    if hasattr(_vtk, "VTK_TRIQUADRATIC_PYRAMID"):
        TRIQUADRATIC_PYRAMID = _vtk.VTK_TRIQUADRATIC_PYRAMID
    QUADRATIC_LINEAR_QUAD = _vtk.VTK_QUADRATIC_LINEAR_QUAD
    QUADRATIC_LINEAR_WEDGE = _vtk.VTK_QUADRATIC_LINEAR_WEDGE
    BIQUADRATIC_QUADRATIC_WEDGE = _vtk.VTK_BIQUADRATIC_QUADRATIC_WEDGE
    BIQUADRATIC_QUADRATIC_HEXAHEDRON = _vtk.VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON
    BIQUADRATIC_TRIANGLE = _vtk.VTK_BIQUADRATIC_TRIANGLE

    # Cubic, isoparametric cell
    CUBIC_LINE = _vtk.VTK_CUBIC_LINE

    # Special class of cells formed by convex group of points
    CONVEX_POINT_SET = _vtk.VTK_CONVEX_POINT_SET

    # Polyhedron cell (consisting of polygonal faces)
    POLYHEDRON = _vtk.VTK_POLYHEDRON

    # Higher order cells in parametric form
    PARAMETRIC_CURVE = _vtk.VTK_PARAMETRIC_CURVE
    PARAMETRIC_SURFACE = _vtk.VTK_PARAMETRIC_SURFACE
    PARAMETRIC_TRI_SURFACE = _vtk.VTK_PARAMETRIC_TRI_SURFACE
    PARAMETRIC_QUAD_SURFACE = _vtk.VTK_PARAMETRIC_QUAD_SURFACE
    PARAMETRIC_TETRA_REGION = _vtk.VTK_PARAMETRIC_TETRA_REGION
    PARAMETRIC_HEX_REGION = _vtk.VTK_PARAMETRIC_HEX_REGION

    # Higher order cells
    HIGHER_ORDER_EDGE = _vtk.VTK_HIGHER_ORDER_EDGE
    HIGHER_ORDER_TRIANGLE = _vtk.VTK_HIGHER_ORDER_TRIANGLE
    HIGHER_ORDER_QUAD = _vtk.VTK_HIGHER_ORDER_QUAD
    HIGHER_ORDER_POLYGON = _vtk.VTK_HIGHER_ORDER_POLYGON
    HIGHER_ORDER_TETRAHEDRON = _vtk.VTK_HIGHER_ORDER_TETRAHEDRON
    HIGHER_ORDER_WEDGE = _vtk.VTK_HIGHER_ORDER_WEDGE
    HIGHER_ORDER_PYRAMID = _vtk.VTK_HIGHER_ORDER_PYRAMID
    HIGHER_ORDER_HEXAHEDRON = _vtk.VTK_HIGHER_ORDER_HEXAHEDRON

    # Arbitrary order Lagrange elements (formulated separated from generic higher order cells)
    LAGRANGE_CURVE = _vtk.VTK_LAGRANGE_CURVE
    LAGRANGE_TRIANGLE = _vtk.VTK_LAGRANGE_TRIANGLE
    LAGRANGE_QUADRILATERAL = _vtk.VTK_LAGRANGE_QUADRILATERAL
    LAGRANGE_TETRAHEDRON = _vtk.VTK_LAGRANGE_TETRAHEDRON
    LAGRANGE_HEXAHEDRON = _vtk.VTK_LAGRANGE_HEXAHEDRON
    LAGRANGE_WEDGE = _vtk.VTK_LAGRANGE_WEDGE
    LAGRANGE_PYRAMID = _vtk.VTK_LAGRANGE_PYRAMID

    # Arbitrary order Bezier elements (formulated separated from generic higher order cells)
    BEZIER_CURVE = _vtk.VTK_BEZIER_CURVE
    BEZIER_TRIANGLE = _vtk.VTK_BEZIER_TRIANGLE
    BEZIER_QUADRILATERAL = _vtk.VTK_BEZIER_QUADRILATERAL
    BEZIER_TETRAHEDRON = _vtk.VTK_BEZIER_TETRAHEDRON
    BEZIER_HEXAHEDRON = _vtk.VTK_BEZIER_HEXAHEDRON
    BEZIER_WEDGE = _vtk.VTK_BEZIER_WEDGE
    BEZIER_PYRAMID = _vtk.VTK_BEZIER_PYRAMID


# pyvista/core/composite.py

"""Container to mimic ``vtkMultiBlockDataSet`` objects.

These classes hold many VTK datasets in one object that can be passed
to VTK algorithms and PyVista filtering/plotting routines.
"""
import collections.abc
from itertools import zip_longest
import pathlib
from typing import Any, Iterable, List, Optional, Set, Tuple, Union, cast, overload

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.utilities import FieldAssociation, is_pyvista_dataset, wrap

from .._typing import BoundsLike
from .dataset import DataObject, DataSet
from .filters import CompositeFilters
from .pyvista_ndarray import pyvista_ndarray

_TypeMultiBlockLeaf = Union['MultiBlock', DataSet]


class MultiBlock(
    _vtk.vtkMultiBlockDataSet, CompositeFilters, DataObject, collections.abc.MutableSequence
):
    """A composite class to hold many data sets which can be iterated over.

    This wraps/extends the `vtkMultiBlockDataSet
    <https://vtk.org/doc/nightly/html/classvtkMultiBlockDataSet.html>`_ class
    so that we can easily plot these data sets and use the composite in a
    Pythonic manner.

    You can think of ``MultiBlock`` like a list as we
    can iterate over this data structure by index.  It has some dictionary
    features as we can also access blocks by their string name.

    .. versionchanged:: 0.36.0
       ``MultiBlock`` adheres more closely to being list like, and inherits
       from :class:`collections.abc.MutableSequence`.  Multiple nonconforming
       behaviors were removed or modified.

    Examples
    --------
    >>> import pyvista as pv

    Create an empty composite dataset.

    >>> blocks = pv.MultiBlock()

    Add a dataset to the collection.

    >>> sphere = pv.Sphere()
    >>> blocks.append(sphere)

    Add a named block.

    >>> blocks["cube"] = pv.Cube()

    Instantiate from a list of objects.

    >>> data = [
    ...     pv.Sphere(center=(2, 0, 0)),
    ...     pv.Cube(center=(0, 2, 0)),
    ...     pv.Cone(),
    ... ]
    >>> blocks = pv.MultiBlock(data)
    >>> blocks.plot()

    Instantiate from a dictionary.

    >>> data = {
    ...     "cube": pv.Cube(),
    ...     "sphere": pv.Sphere(center=(2, 2, 0)),
    ... }
    >>> blocks = pv.MultiBlock(data)
    >>> blocks.plot()

    Iterate over the collection.

    >>> for name in blocks.keys():
    ...     block = blocks[name]
    ...

    >>> for block in blocks:
    ...     # Do something with each dataset
    ...     surf = block.extract_surface()
    ...

    """

    # Bind pyvista.plotting.plot to the object
    plot = pyvista.plot
    _WRITERS = dict.fromkeys(['.vtm', '.vtmb'], _vtk.vtkXMLMultiBlockDataWriter)

    def __init__(self, *args, **kwargs) -> None:
        """Initialize multi block."""
        super().__init__()
        deep = kwargs.pop('deep', False)

        # keep a python reference to the dataset to avoid
        # unintentional garbage collections since python does not
        # add a reference to the dataset when it's added here in
        # MultiBlock.  See https://github.com/pyvista/pyvista/pull/1805
        self._refs: Any = {}

        if len(args) == 1:
            if isinstance(args[0], _vtk.vtkMultiBlockDataSet):
                if deep:
                    self.deep_copy(args[0])
                else:
                    self.shallow_copy(args[0])
            elif isinstance(args[0], (list, tuple)):
                for block in args[0]:
                    self.append(block)
            elif isinstance(args[0], (str, pathlib.Path)):
                self._from_file(args[0], **kwargs)
            elif isinstance(args[0], dict):
                for key, block in args[0].items():
                    self.append(block, key)
            else:
                raise TypeError(f'Type {type(args[0])} is not supported by pyvista.MultiBlock')

        elif len(args) > 1:
            raise ValueError(
                'Invalid number of arguments:\n``pyvista.MultiBlock``' 'supports 0 or 1 arguments.'
            )

        # Upon creation make sure all nested structures are wrapped
        self.wrap_nested()

    def wrap_nested(self):
        """Ensure that all nested data structures are wrapped as PyVista datasets.

        This is performed in place.

        """
        for i in range(self.n_blocks):
            block = self.GetBlock(i)
            if not is_pyvista_dataset(block):
                self.SetBlock(i, pyvista.wrap(block))

    @property
    def bounds(self) -> BoundsLike:
        """Find min/max for bounds across blocks.

        Returns
        -------
        tuple[float, float, float, float, float, float]
            length 6 tuple of floats containing min/max along each axis

        Examples
        --------
        Return the bounds across blocks.

        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.bounds
        (-0.5, 2.5, -0.5, 2.5, -0.5, 0.5)

        """
        # apply reduction of min and max over each block
        # (typing.cast necessary to make mypy happy with ufunc.reduce() later)
        all_bounds = [cast(list, block.bounds) for block in self if block]
        # edge case where block has no bounds
        if not all_bounds:  # pragma: no cover
            minima = np.array([0.0, 0.0, 0.0])
            maxima = np.array([0.0, 0.0, 0.0])
        else:
            minima = np.minimum.reduce(all_bounds)[::2]
            maxima = np.maximum.reduce(all_bounds)[1::2]

        # interleave minima and maxima for bounds
        the_bounds = np.stack([minima, maxima]).ravel('F')

        return cast(BoundsLike, tuple(the_bounds))

    @property
    def center(self) -> Any:
        """Return the center of the bounding box.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.center  # doctest:+SKIP
        array([1., 1., 0.])

        """
        # (typing.cast necessary to make mypy happy with np.reshape())
        return np.reshape(cast(list, self.bounds), (3, 2)).mean(axis=1)

    @property
    def length(self) -> float:
        """Return the length of the diagonal of the bounding box.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.length
        4.3584

        """
        return pyvista.Box(self.bounds).length

    @property
    def n_blocks(self) -> int:
        """Return the total number of blocks set.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.n_blocks
        3

        """
        return self.GetNumberOfBlocks()

    @n_blocks.setter
    def n_blocks(self, n):
        """Change the total number of blocks set."""
        self.SetNumberOfBlocks(n)
        self.Modified()

    @property
    def volume(self) -> float:
        """Return the total volume of all meshes in this dataset.

        Returns
        -------
        float
            Total volume of the mesh.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.volume
        1.7348

        """
        return sum(block.volume for block in self if block)

    def get_data_range(self, name: str, allow_missing: bool = False) -> Tuple[float, float]:  # type: ignore
        """Get the min/max of an array given its name across all blocks.

        Parameters
        ----------
        name : str
            Name of the array.

        allow_missing : bool, default: False
            Allow a block to be missing the named array.

        Returns
        -------
        tuple
            ``(min, max)`` of the named array.

        """
        mini, maxi = np.inf, -np.inf
        for i in range(self.n_blocks):
            data = self[i]
            if data is None:
                continue
            # get the scalars if available - recursive
            try:
                tmi, tma = data.get_data_range(name)
            except KeyError as err:
                if allow_missing:
                    continue
                else:
                    raise err
            if not np.isnan(tmi) and tmi < mini:
                mini = tmi
            if not np.isnan(tma) and tma > maxi:
                maxi = tma
        return mini, maxi

    def get_index_by_name(self, name: str) -> int:
        """Find the index number by block name.

        Parameters
        ----------
        name : str
            Name of the block.

        Returns
        -------
        int
            Index of the block.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.get_index_by_name('sphere')
        1

        """
        for i in range(self.n_blocks):
            if self.get_block_name(i) == name:
                return i
        raise KeyError(f'Block name ({name}) not found')

    @overload
    def __getitem__(self, index: Union[int, str]) -> Optional[_TypeMultiBlockLeaf]:  # noqa: D105
        ...  # pragma: no cover

    @overload
    def __getitem__(self, index: slice) -> 'MultiBlock':  # noqa: D105
        ...  # pragma: no cover

    def __getitem__(self, index):
        """Get a block by its index or name.

        If the name is non-unique then returns the first occurrence.

        """
        if isinstance(index, slice):
            multi = MultiBlock()
            for i in range(self.n_blocks)[index]:
                multi.append(self[i], self.get_block_name(i))
            return multi
        elif isinstance(index, str):
            index = self.get_index_by_name(index)
        ############################
        if index < -self.n_blocks or index >= self.n_blocks:
            raise IndexError(f'index ({index}) out of range for this dataset.')
        if index < 0:
            index = self.n_blocks + index

        data = self.GetBlock(index)
        if data is None:
            return data
        if data is not None and not is_pyvista_dataset(data):
            data = wrap(data)
        return data

    def append(self, dataset: Optional[_TypeMultiBlockLeaf], name: Optional[str] = None):
        """Add a data set to the next block index.

        Parameters
        ----------
        dataset : pyvista.DataSet or pyvista.MultiBlock
            Dataset to append to this multi-block.

        name : str, optional
            Block name to give to dataset.  A default name is given
            depending on the block index as ``'Block-{i:02}'``.

        Examples
        --------
        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.append(pv.Cone())
        >>> len(blocks)
        3
        >>> blocks.append(examples.load_uniform(), "uniform")
        >>> blocks.keys()
        ['cube', 'sphere', 'Block-02', 'uniform']

        """
        # do not allow to add self
        if dataset is self:
            raise ValueError("Cannot nest a composite dataset in itself.")

        index = self.n_blocks  # note off by one so use as index
        # always wrap since we may need to reference the VTK memory address
        if not pyvista.is_pyvista_dataset(dataset):
            dataset = pyvista.wrap(dataset)
        self.n_blocks += 1
        self[index] = dataset
        # No overwrite if name is None
        self.set_block_name(index, name)

    def extend(self, datasets: Iterable[_TypeMultiBlockLeaf]) -> None:
        """Extend MultiBlock with an Iterable.

        If another MultiBlock object is supplied, the key names will
        be preserved.

        Parameters
        ----------
        datasets : Iterable[pyvista.DataSet or pyvista.MultiBlock]
            Datasets to extend.

        Examples
        --------
        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks_uniform = pv.MultiBlock(
        ...     {"uniform": examples.load_uniform()}
        ... )
        >>> blocks.extend(blocks_uniform)
        >>> len(blocks)
        3
        >>> blocks.keys()
        ['cube', 'sphere', 'uniform']

        """
        # Code based on collections.abc
        if isinstance(datasets, MultiBlock):
            for key, data in zip(datasets.keys(), datasets):
                self.append(data, key)
        else:
            for v in datasets:
                self.append(v)

    def get(
        self, index: str, default: Optional[_TypeMultiBlockLeaf] = None
    ) -> Optional[_TypeMultiBlockLeaf]:
        """Get a block by its name.

        If the name is non-unique then returns the first occurrence.
        Returns ``default`` if name isn't in the dataset.

        Parameters
        ----------
        index : str
            Index or name of the dataset within the multiblock.

        default : pyvista.DataSet or pyvista.MultiBlock, optional
            Default to return if index is not in the multiblock.

        Returns
        -------
        pyvista.DataSet or pyvista.MultiBlock or None
            Dataset from the given index if it exists.

        Examples
        --------
        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> data = {"poly": pv.PolyData(), "uni": pv.UniformGrid()}
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.get("poly")
        PolyData ...
        >>> blocks.get("cone")

        """
        try:
            return self[index]
        except KeyError:
            return default

    def set_block_name(self, index: int, name: Optional[str]):
        """Set a block's string name at the specified index.

        Parameters
        ----------
        index : int
            Index or the dataset within the multiblock.

        name : str, optional
            Name to assign to the block at ``index``. If ``None``, no name is
            assigned to the block.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.append(pv.Cone())
        >>> blocks.set_block_name(2, 'cone')
        >>> blocks.keys()
        ['cube', 'sphere', 'cone']

        """
        if name is None:
            return
        index = range(self.n_blocks)[index]
        self.GetMetaData(index).Set(_vtk.vtkCompositeDataSet.NAME(), name)
        self.Modified()

    def get_block_name(self, index: int) -> Optional[str]:
        """Return the string name of the block at the given index.

        Parameters
        ----------
        index : int
            Index of the block to get the name of.

        Returns
        -------
        str
            Name of the block at the given index.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.get_block_name(0)
        'cube'

        """
        index = range(self.n_blocks)[index]
        meta = self.GetMetaData(index)
        if meta is not None:
            return meta.Get(_vtk.vtkCompositeDataSet.NAME())
        return None

    def keys(self) -> List[Optional[str]]:
        """Get all the block names in the dataset.

        Returns
        -------
        list
            List of block names.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.keys()
        ['cube', 'sphere']

        """
        return [self.get_block_name(i) for i in range(self.n_blocks)]

    def _ipython_key_completions_(self) -> List[Optional[str]]:
        return self.keys()

    def replace(self, index: int, dataset: Optional[_TypeMultiBlockLeaf]) -> None:
        """Replace dataset at index while preserving key name.

        Parameters
        ----------
        index : int
            Index of the block to replace.
        dataset : pyvista.DataSet or pyvista.MultiBlock
            Dataset for replacing the one at index.

        Examples
        --------
        >>> import pyvista as pv
        >>> import numpy as np
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.replace(1, pv.Sphere(center=(10, 10, 10)))
        >>> blocks.keys()
        ['cube', 'sphere']
        >>> np.allclose(blocks[1].center, [10.0, 10.0, 10.0])
        True

        """
        name = self.get_block_name(index)
        self[index] = dataset
        self.set_block_name(index, name)

    @overload
    def __setitem__(
        self, index: Union[int, str], data: Optional[_TypeMultiBlockLeaf]
    ):  # noqa: D105
        ...  # pragma: no cover

    @overload
    def __setitem__(
        self, index: slice, data: Iterable[Optional[_TypeMultiBlockLeaf]]
    ):  # noqa: D105
        ...  # pragma: no cover

    def __setitem__(
        self,
        index,
        data,
    ):
        """Set a block with a VTK data object.

        To set the name simultaneously, pass a string name as the 2nd index.

        Example
        -------
        >>> import pyvista
        >>> multi = pyvista.MultiBlock()
        >>> multi.append(pyvista.PolyData())
        >>> multi[0] = pyvista.UnstructuredGrid()
        >>> multi.append(pyvista.PolyData(), 'poly')
        >>> multi.keys()
        ['Block-00', 'poly']
        >>> multi['bar'] = pyvista.PolyData()
        >>> multi.n_blocks
        3

        """
        i: int = 0
        name: Optional[str] = None
        if isinstance(index, str):
            try:
                i = self.get_index_by_name(index)
            except KeyError:
                self.append(data, index)
                return
            name = index
        elif isinstance(index, slice):
            index_iter = range(self.n_blocks)[index]
            for i, (idx, d) in enumerate(zip_longest(index_iter, data)):
                if idx is None:
                    self.insert(
                        index_iter[-1] + 1 + (i - len(index_iter)), d
                    )  # insert after last entry, increasing
                elif d is None:
                    del self[index_iter[-1] + 1]  # delete next entry
                else:
                    self[idx] = d  #
            return
        else:
            i = index

        # data, i, and name are a single value now
        if data is not None and not is_pyvista_dataset(data):
            data = wrap(data)
        data = cast(pyvista.DataSet, data)

        i = range(self.n_blocks)[i]

        # this is the only spot in the class where we actually add
        # data to the MultiBlock

        # check if we are overwriting a block
        existing_dataset = self.GetBlock(i)
        if existing_dataset is not None:
            self._remove_ref(i)
        self.SetBlock(i, data)
        if data is not None:
            self._refs[data.memory_address] = data

        if name is None:
            name = f'Block-{i:02}'
        self.set_block_name(i, name)  # Note that this calls self.Modified()

    def __delitem__(self, index: Union[int, str, slice]) -> None:
        """Remove a block at the specified index."""
        if isinstance(index, slice):
            if index.indices(self.n_blocks)[2] > 0:
                for i in reversed(range(*index.indices(self.n_blocks))):
                    self.__delitem__(i)
            else:
                for i in range(*index.indices(self.n_blocks)):
                    self.__delitem__(i)
            return
        if isinstance(index, str):
            index = self.get_index_by_name(index)
        self._remove_ref(index)
        self.RemoveBlock(index)

    def _remove_ref(self, index: int):
        """Remove python reference to the dataset."""
        dataset = self[index]
        if hasattr(dataset, 'memory_address'):
            self._refs.pop(dataset.memory_address, None)  # type: ignore

    def __iter__(self) -> 'MultiBlock':
        """Return the iterator across all blocks."""
        self._iter_n = 0
        return self

    def __eq__(self, other):
        """Equality comparison."""
        if not isinstance(other, MultiBlock):
            return False

        if self is other:
            return True

        if len(self) != len(other):
            return False

        if not self.keys() == other.keys():
            return False

        if any(self_mesh != other_mesh for self_mesh, other_mesh in zip(self, other)):
            return False

        return True

    def __next__(self) -> Optional[_TypeMultiBlockLeaf]:
        """Get the next block from the iterator."""
        if self._iter_n < self.n_blocks:
            result = self[self._iter_n]
            self._iter_n += 1
            return result
        raise StopIteration

    def insert(self, index: int, dataset: _TypeMultiBlockLeaf, name: Optional[str] = None) -> None:
        """Insert data before index.

        Parameters
        ----------
        index : int
            Index before which to insert data.
        dataset : pyvista.DataSet or pyvista.MultiBlock
            Data to insert.
        name : str, optional
            Name for key to give dataset.  A default name is given
            depending on the block index as ``'Block-{i:02}'``.

        Examples
        --------
        Insert a new :class:`pyvista.PolyData` at the start of the multiblock.

        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.keys()
        ['cube', 'sphere']
        >>> blocks.insert(0, pv.Plane(), "plane")
        >>> blocks.keys()
        ['plane', 'cube', 'sphere']

        """
        index = range(self.n_blocks)[index]

        self.n_blocks += 1
        for i in reversed(range(index, self.n_blocks - 1)):
            self[i + 1] = self[i]
            self.set_block_name(i + 1, self.get_block_name(i))

        self[index] = dataset
        self.set_block_name(index, name)

    def pop(self, index: Union[int, str] = -1) -> Optional[_TypeMultiBlockLeaf]:
        """Pop off a block at the specified index.

        Parameters
        ----------
        index : int or str, default: -1
            Index or name of the dataset within the multiblock.  Defaults to
            last dataset.

        Returns
        -------
        pyvista.DataSet or pyvista.MultiBlock
            Dataset from the given index that was removed.

        Examples
        --------
        Pop the ``"cube"`` multiblock.

        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.keys()
        ['cube', 'sphere']
        >>> cube = blocks.pop("cube")
        >>> blocks.keys()
        ['sphere']

        """
        if isinstance(index, int):
            index = range(self.n_blocks)[index]
        data = self[index]
        del self[index]
        return data

    def reverse(self):
        """Reverse MultiBlock in-place.

        Examples
        --------
        Reverse a multiblock.

        >>> import pyvista as pv
        >>> data = {
        ...     "cube": pv.Cube(),
        ...     "sphere": pv.Sphere(center=(2, 2, 0)),
        ... }
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.keys()
        ['cube', 'sphere']
        >>> blocks.reverse()
        >>> blocks.keys()
        ['sphere', 'cube']

        """
        # Taken from implementation in collections.abc.MutableSequence
        names = self.keys()
        n = len(self)
        for i in range(n // 2):
            self[i], self[n - i - 1] = self[n - i - 1], self[i]
        for i, name in enumerate(reversed(names)):
            self.set_block_name(i, name)

    def clean(self, empty=True):
        """Remove any null blocks in place.

        Parameters
        ----------
        empty : bool, default: True
            Remove any meshes that are empty as well (have zero points).

        Examples
        --------
        >>> import pyvista as pv
        >>> data = {"cube": pv.Cube(), "empty": pv.PolyData()}
        >>> blocks = pv.MultiBlock(data)
        >>> blocks.clean(empty=True)
        >>> blocks.keys()
        ['cube']

        """
        null_blocks = []
        for i in range(self.n_blocks):
            if isinstance(self[i], MultiBlock):
                # Recursively move through nested structures
                self[i].clean()
                if self[i].n_blocks < 1:
                    null_blocks.append(i)
            elif self[i] is None:
                null_blocks.append(i)
            elif empty and self[i].n_points < 1:
                null_blocks.append(i)
        # Now remove the null/empty meshes
        null_blocks = np.array(null_blocks, dtype=int)
        for i in range(len(null_blocks)):
            # Cast as int because windows is super annoying
            del self[int(null_blocks[i])]
            null_blocks -= 1

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = []
        attrs.append(("N Blocks", self.n_blocks, "{}"))
        bds = self.bounds
        attrs.append(("X Bounds", (bds[0], bds[1]), "{:.3f}, {:.3f}"))
        attrs.append(("Y Bounds", (bds[2], bds[3]), "{:.3f}, {:.3f}"))
        attrs.append(("Z Bounds", (bds[4], bds[5]), "{:.3f}, {:.3f}"))
        return attrs

    def _repr_html_(self) -> str:
        """Define a pretty representation for Jupyter notebooks."""
        fmt = ""
        fmt += "<table>"
        fmt += "<tr><th>Information</th><th>Blocks</th></tr>"
        fmt += "<tr><td>"
        fmt += "\n"
        fmt += "<table>\n"
        fmt += f"<tr><th>{type(self).__name__}</th><th>Values</th></tr>\n"
        row = "<tr><td>{}</td><td>{}</td></tr>\n"

        # now make a call on the object to get its attributes as a list of len 2 tuples
        for attr in self._get_attrs():
            try:
                fmt += row.format(attr[0], attr[2].format(*attr[1]))
            except:
                fmt += row.format(attr[0], attr[2].format(attr[1]))

        fmt += "</table>\n"
        fmt += "\n"
        fmt += "</td><td>"
        fmt += "\n"
        fmt += "<table>\n"
        row = "<tr><th>{}</th><th>{}</th><th>{}</th></tr>\n"
        fmt += row.format("Index", "Name", "Type")

        for i in range(self.n_blocks):
            data = self[i]
            fmt += row.format(i, self.get_block_name(i), type(data).__name__)

        fmt += "</table>\n"
        fmt += "\n"
        fmt += "</td></tr> </table>"
        return fmt

    def __repr__(self) -> str:
        """Define an adequate representation."""
        # return a string that is Python console friendly
        fmt = f"{type(self).__name__} ({hex(id(self))})\n"
        # now make a call on the object to get its attributes as a list of len 2 tuples
        max_len = max(len(attr[0]) for attr in self._get_attrs()) + 4
        row = "  {:%ds}{}\n" % max_len
        for attr in self._get_attrs():
            try:
                fmt += row.format(attr[0], attr[2].format(*attr[1]))
            except:
                fmt += row.format(attr[0], attr[2].format(attr[1]))
        return fmt.strip()

    def __str__(self) -> str:
        """Return the str representation of the multi block."""
        return MultiBlock.__repr__(self)

    def __len__(self) -> int:
        """Return the number of blocks."""
        return self.n_blocks

    def copy_meta_from(self, ido, deep):
        """Copy pyvista meta data onto this object from another object."""
        # Note that `pyvista.MultiBlock` datasets currently don't have any meta.
        # This method is here for consistency with the rest of the API and
        # in case we add meta data to this pbject down the road.
        pass

    def copy(self, deep=True):
        """Return a copy of the multiblock.

        Parameters
        ----------
        deep : bool, default: True
            When ``True``, make a full copy of the object.

        Returns
        -------
        pyvista.MultiBlock
           Deep or shallow copy of the ``MultiBlock``.

        Examples
        --------
        >>> import pyvista as pv
        >>> data = [
        ...     pv.Sphere(center=(2, 0, 0)),
        ...     pv.Cube(center=(0, 2, 0)),
        ...     pv.Cone(),
        ... ]
        >>> blocks = pv.MultiBlock(data)
        >>> new_blocks = blocks.copy()
        >>> len(new_blocks)
        3

        """
        thistype = type(self)
        newobject = thistype()
        if deep:
            newobject.deep_copy(self)
        else:
            newobject.shallow_copy(self)
        newobject.copy_meta_from(self, deep)
        newobject.wrap_nested()
        return newobject

    def set_active_scalars(
        self, name: Optional[str], preference: str = 'cell', allow_missing: bool = False
    ) -> Tuple[FieldAssociation, np.ndarray]:  # type: ignore
        """Find the scalars by name and appropriately set it as active.

        To deactivate any active scalars, pass ``None`` as the ``name``.

        Parameters
        ----------
        name : str or None
            Name of the scalars array to assign as active.  If
            ``None``, deactivates active scalars for both point and
            cell data.

        preference : str, default: "cell"
            If there are two arrays of the same name associated with
            points or cells, it will prioritize an array matching this
            type.  Can be either ``'cell'`` or ``'point'``.

        allow_missing : bool, default: False
            Allow missing scalars in part of the composite dataset. If all
            blocks are missing the array, it will raise a ``KeyError``.

        Returns
        -------
        pyvista.utilities.helpers.FieldAssociation
            Field association of the scalars activated.

        numpy.ndarray
            An array from the dataset matching ``name``.

        Notes
        -----
        The number of components of the data must match.

        """
        data_assoc: List[Tuple[FieldAssociation, np.ndarray, _TypeMultiBlockLeaf]] = []
        for block in self:
            if block is not None:
                if isinstance(block, MultiBlock):
                    field, scalars = block.set_active_scalars(
                        name, preference, allow_missing=allow_missing
                    )
                else:
                    try:
                        field, scalars = block.set_active_scalars(name, preference)
                    except KeyError as err:
                        if not allow_missing:
                            raise err
                        block.set_active_scalars(None, preference)
                        field, scalars = FieldAssociation.NONE, pyvista_ndarray([])

                if field != FieldAssociation.NONE:
                    data_assoc.append((field, scalars, block))

        if name is None:
            return FieldAssociation.NONE, pyvista_ndarray([])

        if not data_assoc:
            raise KeyError(f'"{name}" is missing from all the blocks of this composite dataset.')

        field_asc = data_assoc[0][0]
        # set the field association to the preference if at least one occurrence
        # of it exists
        if field_asc.name.lower() != preference.lower():
            for field, _, _ in data_assoc:
                if field.name.lower() == preference:
                    field_asc = getattr(FieldAssociation, preference.upper())
                    break

        # Verify array consistency
        dims: Set[int] = set()
        dtypes: Set[np.dtype] = set()
        for block in self:
            for field, scalars, _ in data_assoc:
                # only check for the active field association
                if field != field_asc:
                    continue
                dims.add(scalars.ndim)
                dtypes.add(scalars.dtype)

        if len(dims) > 1:
            raise ValueError(f'Inconsistent dimensions {dims} in active scalars.')

        # check complex mismatch
        is_complex = [np.issubdtype(dtype, np.complexfloating) for dtype in dtypes]
        if any(is_complex) and not all(is_complex):
            raise ValueError('Inconsistent complex and real data types in active scalars.')

        return field_asc, scalars

    def as_polydata_blocks(self, copy=False):
        """Convert all the datasets within this MultiBlock to :class:`pyvista.PolyData`.

        Parameters
        ----------
        copy : bool, default: False
            Option to create a shallow copy of any datasets that are already a
            :class:`pyvista.PolyData`. When ``False``, any datasets that are
            already PolyData will not be copied.

        Returns
        -------
        pyvista.MultiBlock
            MultiBlock containing only :class:`pyvista.PolyData` datasets.

        Notes
        -----
        Null blocks are converted to empty :class:`pyvista.PolyData`
        objects. Downstream filters that operate on PolyData cannot accept
        MultiBlocks with null blocks.

        """
        # we make a shallow copy here to avoid modifying the original dataset
        dataset = self.copy(deep=False)

        # Loop through the multiblock and convert to polydata
        for i, block in enumerate(dataset):
            if block is not None:
                if isinstance(block, MultiBlock):
                    dataset.replace(i, block.as_polydata_blocks(copy=copy))
                elif isinstance(block, pyvista.PointSet):
                    dataset.replace(i, block.cast_to_polydata(deep=True))
                elif not isinstance(block, pyvista.PolyData):
                    dataset.replace(i, block.extract_surface())
                elif copy:
                    # dataset is a PolyData
                    dataset.replace(i, block.copy(deep=False))
            else:
                # must have empty polydata within these datasets as some
                # downstream filters don't work on null pointers (i.e. None)
                dataset[i] = pyvista.PolyData()

        return dataset

    @property
    def is_all_polydata(self) -> bool:
        """Return ``True`` when all the blocks are :class:`pyvista.PolyData`.

        This method will recursively check if any internal blocks are also
        :class:`pyvista.PolyData`.

        Returns
        -------
        bool
            Return ``True`` when all blocks are :class:`pyvista.PolyData`.

        """
        for block in self:
            if isinstance(block, MultiBlock):
                if not block.is_all_polydata:
                    return False
            else:
                if not isinstance(block, pyvista.PolyData):
                    return False

        return True

    def _activate_plotting_scalars(self, scalars_name, preference, component, rgb):
        """Active a scalars for an instance of :class:`pyvista.Plotter`."""
        # set the active scalars
        field, scalars = self.set_active_scalars(
            scalars_name,
            preference,
            allow_missing=True,
        )

        data_attr = f'{field.name.lower()}_data'
        dtype = scalars.dtype
        if rgb:
            if scalars.ndim != 2 or scalars.shape[1] not in (3, 4):
                raise ValueError('RGB array must be n_points/n_cells by 3/4 in shape.')
        elif np.issubdtype(scalars.dtype, np.complexfloating):
            # Use only the real component if an array is complex
            scalars_name = self._convert_to_real_scalars(data_attr, scalars_name)
        elif scalars.dtype in (np.bool_, np.uint8):
            # bool and uint8 do not display properly, must convert to float
            self._convert_to_real_scalars(data_attr, scalars_name)
            if scalars.dtype == np.bool_:
                dtype = np.bool_
        elif scalars.ndim > 1:
            # multi-component
            if not isinstance(component, (int, type(None))):
                raise TypeError('`component` must be either None or an integer')
            if component is not None:
                if component >= scalars.shape[1] or component < 0:
                    raise ValueError(
                        'Component must be nonnegative and less than the '
                        f'dimensionality of the scalars array: {scalars.shape[1]}'
                    )
            scalars_name = self._convert_to_single_component(data_attr, scalars_name, component)

        return field, scalars_name, dtype

    def _convert_to_real_scalars(self, data_attr: str, scalars_name: str):
        """Extract the real component of the active scalars of this dataset."""
        for block in self:
            if isinstance(block, MultiBlock):
                block._convert_to_real_scalars(data_attr, scalars_name)
            elif block is not None:
                scalars = getattr(block, data_attr).get(scalars_name, None)
                if scalars is not None:
                    scalars = np.array(scalars.astype(float))
                    dattr = getattr(block, data_attr)
                    dattr[f'{scalars_name}-real'] = scalars
                    dattr.active_scalars_name = f'{scalars_name}-real'
        return f'{scalars_name}-real'

    def _convert_to_single_component(
        self, data_attr: str, scalars_name: str, component: Union[None, str]
    ) -> str:
        """Convert multi-component scalars to a single component."""
        if component is None:
            for block in self:
                if isinstance(block, MultiBlock):
                    block._convert_to_single_component(data_attr, scalars_name, component)
                elif block is not None:
                    scalars = getattr(block, data_attr).get(scalars_name, None)
                    if scalars is not None:
                        scalars = np.linalg.norm(scalars, axis=1)
                        dattr = getattr(block, data_attr)
                        dattr[f'{scalars_name}-normed'] = scalars
                        dattr.active_scalars_name = f'{scalars_name}-normed'
            return f'{scalars_name}-normed'

        for block in self:
            if isinstance(block, MultiBlock):
                block._convert_to_single_component(data_attr, scalars_name, component)
            elif block is not None:
                scalars = getattr(block, data_attr).get(scalars_name, None)
                if scalars is not None:
                    dattr = getattr(block, data_attr)
                    dattr[f'{scalars_name}-{component}'] = scalars[:, component]
                    dattr.active_scalars_name = f'{scalars_name}-{component}'
        return f'{scalars_name}-{component}'

    def _get_consistent_active_scalars(self):
        """Check if there are any consistent active scalars."""
        point_names = set()
        cell_names = set()
        for block in self:
            if isinstance(block, MultiBlock):
                point_name, cell_name = block._get_consistent_active_scalars()
            elif block is not None:
                point_name = block.point_data.active_scalars_name
                cell_name = block.cell_data.active_scalars_name
            point_names.add(point_name)
            cell_names.add(cell_name)

        point_name = point_names.pop() if len(point_names) == 1 else None
        cell_name = cell_names.pop() if len(cell_names) == 1 else None
        return point_name, cell_name


# pyvista/core/dataobject.py

"""Attributes common to PolyData and Grid Objects."""

from abc import abstractmethod
import collections.abc
from pathlib import Path
from typing import Any, DefaultDict, Dict, Type, Union

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.utilities import FieldAssociation, abstract_class, fileio

from .datasetattributes import DataSetAttributes

# vector array names
DEFAULT_VECTOR_KEY = '_vectors'


@abstract_class
class DataObject:
    """Methods common to all wrapped data objects."""

    _WRITERS: Dict[str, Union[Type[_vtk.vtkXMLWriter], Type[_vtk.vtkDataWriter]]] = {}

    def __init__(self, *args, **kwargs) -> None:
        """Initialize the data object."""
        super().__init__()
        # Remember which arrays come from numpy.bool arrays, because there is no direct
        # conversion from bool to vtkBitArray, such arrays are stored as vtkCharArray.
        self._association_bitarray_names: DefaultDict = collections.defaultdict(set)

        # view these arrays as complex128 as VTK doesn't support complex types
        self._association_complex_names: DefaultDict = collections.defaultdict(set)

    def __getattr__(self, item: str) -> Any:
        """Get attribute from base class if not found."""
        return super().__getattribute__(item)

    def shallow_copy(self, to_copy: _vtk.vtkDataObject) -> _vtk.vtkDataObject:
        """Shallow copy the given mesh to this mesh.

        Parameters
        ----------
        to_copy : pyvista.DataObject or vtk.vtkDataObject
            Data object to perform a shallow copy from.

        """
        self.ShallowCopy(to_copy)

    def deep_copy(self, to_copy: _vtk.vtkDataObject) -> _vtk.vtkDataObject:
        """Overwrite this data object with another data object as a deep copy.

        Parameters
        ----------
        to_copy : pyvista.DataObject or vtk.vtkDataObject
            Data object to perform a deep copy from.

        """
        self.DeepCopy(to_copy)

    def _from_file(self, filename: Union[str, Path], **kwargs):
        data = pyvista.read(filename, **kwargs)
        if not isinstance(self, type(data)):
            raise ValueError(
                f'Reading file returned data of `{type(data).__name__}`, '
                f'but `{type(self).__name__}` was expected.'
            )
        self.shallow_copy(data)
        self._post_file_load_processing()

    def _post_file_load_processing(self):
        """Execute after loading a dataset from file, to be optionally overridden by subclasses."""
        pass

    def save(self, filename: str, binary=True, texture=None):
        """Save this vtk object to file.

        Parameters
        ----------
        filename : str, pathlib.Path
            Filename of output file. Writer type is inferred from
            the extension of the filename.

        binary : bool, default: True
            If ``True``, write as binary.  Otherwise, write as ASCII.

        texture : str, np.ndarray, optional
            Write a single texture array to file when using a PLY
            file.  Texture array must be a 3 or 4 component array with
            the datatype ``np.uint8``.  Array may be a cell array or a
            point array, and may also be a string if the array already
            exists in the PolyData.

            If a string is provided, the texture array will be saved
            to disk as that name.  If an array is provided, the
            texture array will be saved as ``'RGBA'``

            .. note::
               This feature is only available when saving PLY files.

        Notes
        -----
        Binary files write much faster than ASCII and have a smaller
        file size.

        """
        if self._WRITERS is None:
            raise NotImplementedError(
                f'{self.__class__.__name__} writers are not specified,'
                ' this should be a dict of (file extension: vtkWriter type)'
            )

        file_path = Path(filename)
        file_path = file_path.expanduser()
        file_path = file_path.resolve()
        file_ext = file_path.suffix
        if file_ext not in self._WRITERS:
            raise ValueError(
                'Invalid file extension for this data type.'
                f' Must be one of: {self._WRITERS.keys()}'
            )

        # store complex and bitarray types as field data
        self._store_metadata()

        writer = self._WRITERS[file_ext]()
        fileio.set_vtkwriter_mode(vtk_writer=writer, use_binary=binary)
        writer.SetFileName(str(file_path))
        writer.SetInputData(self)
        if file_ext == '.ply' and texture is not None:
            if isinstance(texture, str):
                writer.SetArrayName(texture)
                array_name = texture
            elif isinstance(texture, np.ndarray):
                array_name = '_color_array'
                self[array_name] = texture
                writer.SetArrayName(array_name)

            # enable alpha channel if applicable
            if self[array_name].shape[-1] == 4:  # type: ignore
                writer.SetEnableAlpha(True)
        writer.Write()

    def _store_metadata(self):
        """Store metadata as field data."""
        fdata = self.field_data
        for assoc_name in ('bitarray', 'complex'):
            for assoc_type in ('POINT', 'CELL'):
                assoc_data = getattr(self, f'_association_{assoc_name}_names')
                array_names = assoc_data.get(assoc_type)
                if array_names:
                    key = f'_PYVISTA_{assoc_name}_{assoc_type}_'.upper()
                    fdata[key] = list(array_names)

    def _restore_metadata(self):
        """Restore PyVista metadata from field data.

        Metadata is stored using ``_store_metadata`` and contains entries in
        the format of f'_PYVISTA_{assoc_name}_{assoc_type}_'. These entries are
        removed when calling this method.

        """
        fdata = self.field_data
        for assoc_name in ('bitarray', 'complex'):
            for assoc_type in ('POINT', 'CELL'):
                key = f'_PYVISTA_{assoc_name}_{assoc_type}_'.upper()
                if key in fdata:
                    assoc_data = getattr(self, f'_association_{assoc_name}_names')
                    assoc_data[assoc_type] = set(fdata[key])
                    del fdata[key]

    @abstractmethod
    def get_data_range(self):  # pragma: no cover
        """Get the non-NaN min and max of a named array."""
        raise NotImplementedError(
            f'{type(self)} mesh type does not have a `get_data_range` method.'
        )

    def _get_attrs(self):  # pragma: no cover
        """Return the representation methods (internal helper)."""
        raise NotImplementedError('Called only by the inherited class')

    def head(self, display=True, html=None):
        """Return the header stats of this dataset.

        If in IPython, this will be formatted to HTML. Otherwise
        returns a console friendly string.

        Parameters
        ----------
        display : bool, default: True
            Display this header in iPython.

        html : bool, optional
            Generate the output as HTML.

        Returns
        -------
        str
            Header statistics.

        """
        # Generate the output
        if html:
            fmt = ""
            # HTML version
            fmt += "\n"
            fmt += "<table>\n"
            fmt += f"<tr><th>{type(self).__name__}</th><th>Information</th></tr>\n"
            row = "<tr><td>{}</td><td>{}</td></tr>\n"
            # now make a call on the object to get its attributes as a list of len 2 tuples
            for attr in self._get_attrs():
                try:
                    fmt += row.format(attr[0], attr[2].format(*attr[1]))
                except:
                    fmt += row.format(attr[0], attr[2].format(attr[1]))
            if hasattr(self, 'n_arrays'):
                fmt += row.format('N Arrays', self.n_arrays)
            fmt += "</table>\n"
            fmt += "\n"
            if display:
                from IPython.display import HTML, display as _display

                _display(HTML(fmt))
                return
            return fmt
        # Otherwise return a string that is Python console friendly
        fmt = f"{type(self).__name__} ({hex(id(self))})\n"
        # now make a call on the object to get its attributes as a list of len 2 tuples
        # get longest row header
        max_len = max(len(attr[0]) for attr in self._get_attrs()) + 4

        # now make a call on the object to get its attributes as a list of len
        # 2 tuples
        row = "  {:%ds}{}\n" % max_len
        for attr in self._get_attrs():
            try:
                fmt += row.format(attr[0] + ':', attr[2].format(*attr[1]))
            except:
                fmt += row.format(attr[0] + ':', attr[2].format(attr[1]))
        if hasattr(self, 'n_arrays'):
            fmt += row.format('N Arrays:', self.n_arrays)
        return fmt.strip()

    def _repr_html_(self):  # pragma: no cover
        """Return a pretty representation for Jupyter notebooks.

        This includes header details and information about all arrays.

        """
        raise NotImplementedError('Called only by the inherited class')

    def copy_meta_from(self, *args, **kwargs):  # pragma: no cover
        """Copy pyvista meta data onto this object from another object.

        Intended to be overridden by subclasses.

        Parameters
        ----------
        *args : tuple
            Positional arguments.

        **kwargs : dict, optional
            Keyword arguments.

        """
        pass  # called only by the inherited class

    def copy(self, deep=True):
        """Return a copy of the object.

        Parameters
        ----------
        deep : bool, default: True
            When ``True`` makes a full copy of the object.  When
            ``False``, performs a shallow copy where the points, cell,
            and data arrays are references to the original object.

        Returns
        -------
        pyvista.DataSet
            Deep or shallow copy of the input.  Type is identical to
            the input.

        Examples
        --------
        Create and make a deep copy of a PolyData object.

        >>> import pyvista
        >>> mesh_a = pyvista.Sphere()
        >>> mesh_b = mesh_a.copy()
        >>> mesh_a == mesh_b
        True

        """
        thistype = type(self)
        newobject = thistype()

        if deep:
            newobject.deep_copy(self)
        else:
            newobject.shallow_copy(self)
        newobject.copy_meta_from(self, deep)
        return newobject

    def __eq__(self, other):
        """Test equivalency between data objects."""
        if not isinstance(self, type(other)):
            return False

        if self is other:
            return True

        # these attrs use numpy.array_equal
        equal_attrs = [
            'verts',  # DataObject
            'points',  # DataObject
            'lines',  # DataObject
            'faces',  # DataObject
            'cells',  # UnstructuredGrid
            'celltypes',
        ]  # UnstructuredGrid
        for attr in equal_attrs:
            if hasattr(self, attr):
                if not np.array_equal(getattr(self, attr), getattr(other, attr)):
                    return False

        # these attrs can be directly compared
        attrs = ['field_data', 'point_data', 'cell_data']
        for attr in attrs:
            if hasattr(self, attr):
                if getattr(self, attr) != getattr(other, attr):
                    return False

        return True

    def add_field_data(self, array: np.ndarray, name: str, deep=True):
        """Add field data.

        Use field data when size of the data you wish to associate
        with the dataset does not match the number of points or cells
        of the dataset.

        Parameters
        ----------
        array : sequence
            Array of data to add to the dataset as a field array.

        name : str
            Name to assign the field array.

        deep : bool, default: True
            Perform a deep copy of the data when adding it to the
            dataset.

        Examples
        --------
        Add field data to a PolyData dataset.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Sphere()
        >>> mesh.add_field_data(np.arange(10), 'my-field-data')
        >>> mesh['my-field-data']
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

        Add field data to a UniformGrid dataset.

        >>> mesh = pyvista.UniformGrid(dimensions=(2, 2, 1))
        >>> mesh.add_field_data(
        ...     ['I could', 'write', 'notes', 'here'], 'my-field-data'
        ... )
        >>> mesh['my-field-data']
        pyvista_ndarray(['I could', 'write', 'notes', 'here'], dtype='<U7')

        Add field data to a MultiBlock dataset.

        >>> blocks = pyvista.MultiBlock()
        >>> blocks.append(pyvista.Sphere())
        >>> blocks["cube"] = pyvista.Cube(center=(0, 0, -1))
        >>> blocks.add_field_data([1, 2, 3], 'my-field-data')
        >>> blocks.field_data['my-field-data']
        pyvista_ndarray([1, 2, 3])

        """
        if not hasattr(self, 'field_data'):
            raise NotImplementedError(f'`{type(self)}` does not support field data')

        self.field_data.set_array(array, name, deep_copy=deep)

    @property
    def field_data(self) -> DataSetAttributes:
        """Return FieldData as DataSetAttributes.

        Use field data when size of the data you wish to associate
        with the dataset does not match the number of points or cells
        of the dataset.

        Examples
        --------
        Add field data to a PolyData dataset and then return it.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Sphere()
        >>> mesh.field_data['my-field-data'] = np.arange(10)
        >>> mesh.field_data['my-field-data']
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

        """
        return DataSetAttributes(
            self.GetFieldData(), dataset=self, association=FieldAssociation.NONE
        )

    def clear_field_data(self):
        """Remove all field data.

        Examples
        --------
        Add field data to a PolyData dataset and then remove it.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.field_data['my-field-data'] = range(10)
        >>> len(mesh.field_data)
        1
        >>> mesh.clear_field_data()
        >>> len(mesh.field_data)
        0

        """
        if not hasattr(self, 'field_data'):
            raise NotImplementedError(f'`{type(self)}` does not support field data')

        self.field_data.clear()

    @property
    def memory_address(self) -> str:
        """Get address of the underlying VTK C++ object.

        Returns
        -------
        str
            Memory address formatted as ``'Addr=%p'``.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.memory_address
        'Addr=...'

        """
        return self.GetInformation().GetAddressAsString("")

    @property
    def actual_memory_size(self) -> int:
        """Return the actual size of the dataset object.

        Returns
        -------
        int
            The actual size of the dataset object in kibibytes (1024
            bytes).

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()
        >>> mesh.actual_memory_size  # doctest:+SKIP
        93

        """
        return self.GetActualMemorySize()

    def copy_structure(self, dataset: _vtk.vtkDataSet):
        """Copy the structure (geometry and topology) of the input dataset object.

        Parameters
        ----------
        dataset : vtk.vtkDataSet
            Dataset to copy the geometry and topology from.

        Examples
        --------
        >>> import pyvista as pv
        >>> source = pv.UniformGrid(dimensions=(10, 10, 5))
        >>> target = pv.UniformGrid()
        >>> target.copy_structure(source)
        >>> target.plot(show_edges=True)

        """
        self.CopyStructure(dataset)

    def copy_attributes(self, dataset: _vtk.vtkDataSet):
        """Copy the data attributes of the input dataset object.

        Parameters
        ----------
        dataset : pyvista.DataSet
            Dataset to copy the data attributes from.

        Examples
        --------
        >>> import pyvista as pv
        >>> source = pv.UniformGrid(dimensions=(10, 10, 5))
        >>> source = source.compute_cell_sizes()
        >>> target = pv.UniformGrid(dimensions=(10, 10, 5))
        >>> target.copy_attributes(source)
        >>> target.plot(scalars='Volume', show_edges=True)

        """
        self.CopyAttributes(dataset)

    def __getstate__(self):
        """Support pickle by serializing the VTK object data to something which can be pickled natively.

        The format of the serialized VTK object data depends on `pyvista.PICKLE_FORMAT` (case-insensitive).
        - If `pyvista.PICKLE_FORMAT == 'xml'`, the data is serialized as an XML-formatted string.
        - If `pyvista.PICKLE_FORMAT == 'legacy'`, the data is serialized to bytes in VTK's binary format.
        """
        state = self.__dict__.copy()

        if pyvista.PICKLE_FORMAT.lower() == 'xml':
            # the generic VTK XML writer `vtkXMLDataSetWriter` currently has a bug where it does not pass all
            # settings down to the sub-writers. Until this is fixed, use the dataset-specific writers
            # https://gitlab.kitware.com/vtk/vtk/-/issues/18661
            writers = {
                _vtk.vtkImageData: _vtk.vtkXMLImageDataWriter,
                _vtk.vtkStructuredGrid: _vtk.vtkXMLStructuredGridWriter,
                _vtk.vtkRectilinearGrid: _vtk.vtkXMLRectilinearGridWriter,
                _vtk.vtkUnstructuredGrid: _vtk.vtkXMLUnstructuredGridWriter,
                _vtk.vtkPolyData: _vtk.vtkXMLPolyDataWriter,
                _vtk.vtkTable: _vtk.vtkXMLTableWriter,
            }

            for parent_type, writer_type in writers.items():
                if isinstance(self, parent_type):
                    writer = writer_type()
                    break
            else:
                raise TypeError(f'Cannot pickle dataset of type {self.GetDataObjectType()}')

            writer.SetInputDataObject(self)
            writer.SetWriteToOutputString(True)
            writer.SetDataModeToBinary()
            writer.SetCompressorTypeToNone()
            writer.Write()
            to_serialize = writer.GetOutputString()

        elif pyvista.PICKLE_FORMAT.lower() == 'legacy':
            writer = _vtk.vtkDataSetWriter()
            writer.SetInputDataObject(self)
            writer.SetWriteToOutputString(True)
            writer.SetFileTypeToBinary()
            writer.Write()
            to_serialize = writer.GetOutputStdString()

        state['vtk_serialized'] = to_serialize

        # this needs to be here because in multiprocessing situations, `pyvista.PICKLE_FORMAT` is not shared between
        # processes
        state['PICKLE_FORMAT'] = pyvista.PICKLE_FORMAT
        return state

    def __setstate__(self, state):
        """Support unpickle."""
        vtk_serialized = state.pop('vtk_serialized')
        pickle_format = state.pop(
            'PICKLE_FORMAT', 'legacy'  # backwards compatibility - assume 'legacy'
        )
        self.__dict__.update(state)

        if pickle_format.lower() == 'xml':
            # the generic VTK XML reader `vtkXMLGenericDataObjectReader` currently has a bug where it does not pass all
            # settings down to the sub-readers. Until this is fixed, use the dataset-specific readers
            # https://gitlab.kitware.com/vtk/vtk/-/issues/18661
            readers = {
                _vtk.vtkImageData: _vtk.vtkXMLImageDataReader,
                _vtk.vtkStructuredGrid: _vtk.vtkXMLStructuredGridReader,
                _vtk.vtkRectilinearGrid: _vtk.vtkXMLRectilinearGridReader,
                _vtk.vtkUnstructuredGrid: _vtk.vtkXMLUnstructuredGridReader,
                _vtk.vtkPolyData: _vtk.vtkXMLPolyDataReader,
                _vtk.vtkTable: _vtk.vtkXMLTableReader,
            }

            for parent_type, reader_type in readers.items():
                if isinstance(self, parent_type):
                    reader = reader_type()
                    break
            else:
                raise TypeError(f'Cannot unpickle dataset of type {self.GetDataObjectType()}')

            reader.ReadFromInputStringOn()
            reader.SetInputString(vtk_serialized)
            reader.Update()

        elif pickle_format.lower() == 'legacy':
            reader = _vtk.vtkDataSetReader()
            reader.ReadFromInputStringOn()
            if isinstance(vtk_serialized, bytes):
                reader.SetBinaryInputString(vtk_serialized, len(vtk_serialized))
            elif isinstance(vtk_serialized, str):
                reader.SetInputString(vtk_serialized)
            reader.Update()

        mesh = pyvista.wrap(reader.GetOutput())

        # copy data
        self.copy_structure(mesh)
        self.copy_attributes(mesh)


# pyvista/core/dataset.py

"""Attributes common to PolyData and Grid Objects."""
from __future__ import annotations

from collections import namedtuple
import collections.abc
from copy import deepcopy
from functools import partial
from typing import (
    Any,
    Callable,
    Dict,
    Generator,
    Iterable,
    Iterator,
    List,
    Literal,
    Optional,
    Tuple,
    Union,
    cast,
)
import warnings

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.core.errors import VTKVersionError
from pyvista.utilities import (
    FieldAssociation,
    abstract_class,
    get_array,
    get_array_association,
    is_pyvista_dataset,
    raise_not_matching,
    transformations,
    vtk_id_list_to_array,
)
from pyvista.utilities.arrays import _coerce_pointslike_arg
from pyvista.utilities.errors import check_valid_vector
from pyvista.utilities.misc import PyVistaDeprecationWarning

from .._typing import BoundsLike, Number, NumericArray, Vector, VectorArray
from .dataobject import DataObject
from .datasetattributes import DataSetAttributes
from .filters import DataSetFilters, _get_output
from .pyvista_ndarray import pyvista_ndarray

# vector array names
DEFAULT_VECTOR_KEY = '_vectors'
ActiveArrayInfoTuple = namedtuple('ActiveArrayInfoTuple', ['association', 'name'])


class ActiveArrayInfo:
    """Active array info class with support for pickling."""

    def __init__(self, association, name):
        """Initialize."""
        self.association = association
        self.name = name

    def copy(self):
        """Return a copy of this object.

        Returns
        -------
        ActiveArrayInfo
            A copy of this object.

        """
        return ActiveArrayInfo(self.association, self.name)

    def __getstate__(self):
        """Support pickling."""
        state = self.__dict__.copy()
        state['association'] = int(self.association.value)
        return state

    def __setstate__(self, state):
        """Support unpickling."""
        self.__dict__ = state.copy()
        self.association = FieldAssociation(state['association'])

    @property
    def _namedtuple(self):
        """Build a namedtuple on the fly to provide legacy support."""
        return ActiveArrayInfoTuple(self.association, self.name)

    def __iter__(self):
        """Provide namedtuple-like __iter__."""
        return self._namedtuple.__iter__()

    def __repr__(self):
        """Provide namedtuple-like __repr__."""
        return self._namedtuple.__repr__()

    def __getitem__(self, item):
        """Provide namedtuple-like __getitem__."""
        return self._namedtuple.__getitem__(item)

    def __setitem__(self, key, value):
        """Provide namedtuple-like __setitem__."""
        self._namedtuple.__setitem__(key, value)

    def __getattr__(self, item):
        """Provide namedtuple-like __getattr__."""
        self._namedtuple.__getattr__(item)

    def __eq__(self, other):
        """Check equivalence (useful for serialize/deserialize tests)."""
        same_association = int(self.association.value) == int(other.association.value)
        return self.name == other.name and same_association


@abstract_class
class DataSet(DataSetFilters, DataObject):
    """Methods in common to spatially referenced objects."""

    # Simply bind pyvista.plotting.plot to the object
    plot = pyvista.plot

    def __init__(self, *args, **kwargs) -> None:
        """Initialize the common object."""
        super().__init__()
        self._last_active_scalars_name: Optional[str] = None
        self._active_scalars_info = ActiveArrayInfo(FieldAssociation.POINT, name=None)
        self._active_vectors_info = ActiveArrayInfo(FieldAssociation.POINT, name=None)
        self._active_tensors_info = ActiveArrayInfo(FieldAssociation.POINT, name=None)
        self._textures: Dict[str, _vtk.vtkTexture] = {}

    def __getattr__(self, item) -> Any:
        """Get attribute from base class if not found."""
        return super().__getattribute__(item)

    @property
    def active_scalars_info(self) -> ActiveArrayInfo:
        """Return the active scalar's association and name.

        Association refers to the data association (e.g. point, cell, or
        field) of the active scalars.

        Returns
        -------
        ActiveArrayInfo
            The scalars info in an object with namedtuple semantics,
            with attributes ``association`` and ``name``.

        Notes
        -----
        If both cell and point scalars are present and neither have
        been set active within at the dataset level, point scalars
        will be made active.

        Examples
        --------
        Create a mesh, add scalars to the mesh, and return the active
        scalars info.  Note how when the scalars are added, they
        automatically become the active scalars.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh['Z Height'] = mesh.points[:, 2]
        >>> mesh.active_scalars_info
        ActiveArrayInfoTuple(association=<FieldAssociation.POINT: 0>, name='Z Height')

        """
        field, name = self._active_scalars_info
        exclude = {'__custom_rgba', 'Normals', 'vtkOriginalPointIds', 'TCoords'}
        if name in exclude:
            name = self._last_active_scalars_name

        # verify this field is still valid
        if name is not None:
            if field is FieldAssociation.CELL:
                if self.cell_data.active_scalars_name != name:
                    name = None
            elif field is FieldAssociation.POINT:
                if self.point_data.active_scalars_name != name:
                    name = None

        if name is None:
            # check for the active scalars in point or cell arrays
            self._active_scalars_info = ActiveArrayInfo(field, None)
            for attr in [self.point_data, self.cell_data]:
                if attr.active_scalars_name is not None:
                    self._active_scalars_info = ActiveArrayInfo(
                        attr.association, attr.active_scalars_name
                    )
                    break

        return self._active_scalars_info

    @property
    def active_vectors_info(self) -> ActiveArrayInfo:
        """Return the active vector's association and name.

        Association refers to the data association (e.g. point, cell, or
        field) of the active vectors.

        Returns
        -------
        ActiveArrayInfo
            The vectors info in an object with namedtuple semantics,
            with attributes ``association`` and ``name``.

        Notes
        -----
        If both cell and point vectors are present and neither have
        been set active within at the dataset level, point vectors
        will be made active.

        Examples
        --------
        Create a mesh, compute the normals inplace, set the active
        vectors to the normals, and show that the active vectors are
        the ``'Normals'`` array associated with points.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> _ = mesh.compute_normals(inplace=True)
        >>> mesh.active_vectors_name = 'Normals'
        >>> mesh.active_vectors_info
        ActiveArrayInfoTuple(association=<FieldAssociation.POINT: 0>, name='Normals')

        """
        field, name = self._active_vectors_info

        # verify this field is still valid
        if name is not None:
            if field is FieldAssociation.POINT:
                if self.point_data.active_vectors_name != name:
                    name = None
            if field is FieldAssociation.CELL:
                if self.cell_data.active_vectors_name != name:
                    name = None

        if name is None:
            # check for the active vectors in point or cell arrays
            self._active_vectors_info = ActiveArrayInfo(field, None)
            for attr in [self.point_data, self.cell_data]:
                name = attr.active_vectors_name
                if name is not None:
                    self._active_vectors_info = ActiveArrayInfo(attr.association, name)
                    break

        return self._active_vectors_info

    @property
    def active_tensors_info(self) -> ActiveArrayInfo:
        """Return the active tensor's field and name: [field, name]."""
        return self._active_tensors_info

    @property
    def active_vectors(self) -> Optional[pyvista_ndarray]:
        """Return the active vectors array.

        Examples
        --------
        Create a mesh, compute the normals inplace, and return the
        normals vector array.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> _ = mesh.compute_normals(inplace=True)
        >>> mesh.active_vectors  # doctest:+SKIP
        pyvista_ndarray([[-2.48721432e-10, -1.08815623e-09, -1.00000000e+00],
                         [-2.48721432e-10, -1.08815623e-09,  1.00000000e+00],
                         [-1.18888125e-01,  3.40539310e-03, -9.92901802e-01],
                         ...,
                         [-3.11940581e-01, -6.81432486e-02,  9.47654784e-01],
                         [-2.09880397e-01, -4.65070531e-02,  9.76620376e-01],
                         [-1.15582108e-01, -2.80492082e-02,  9.92901802e-01]],
                        dtype=float32)

        """
        field, name = self.active_vectors_info
        if name is not None:
            try:
                if field is FieldAssociation.POINT:
                    return self.point_data[name]
                if field is FieldAssociation.CELL:
                    return self.cell_data[name]
            except KeyError:
                return None
        return None

    @property
    def active_tensors(self) -> Optional[np.ndarray]:
        """Return the active tensors array."""
        field, name = self.active_tensors_info
        if name is not None:
            try:
                if field is FieldAssociation.POINT:
                    return self.point_data[name]
                if field is FieldAssociation.CELL:
                    return self.cell_data[name]
            except KeyError:
                return None
        return None

    @property
    def active_tensors_name(self) -> str:
        """Return the name of the active tensor array."""
        return self.active_tensors_info.name

    @active_tensors_name.setter
    def active_tensors_name(self, name: str):
        self.set_active_tensors(name)

    @property
    def active_vectors_name(self) -> str:
        """Return the name of the active vectors array.

        Examples
        --------
        Create a mesh, compute the normals, set them as active, and
        return the name of the active vectors.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh_w_normals = mesh.compute_normals()
        >>> mesh_w_normals.active_vectors_name = 'Normals'
        >>> mesh_w_normals.active_vectors_name
        'Normals'

        """
        return self.active_vectors_info.name

    @active_vectors_name.setter
    def active_vectors_name(self, name: str):
        self.set_active_vectors(name)

    @property  # type: ignore
    def active_scalars_name(self) -> str:  # type: ignore
        """Return the name of the active scalars.

        Examples
        --------
        Create a mesh, add scalars to the mesh, and return the name of
        the active scalars.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh['Z Height'] = mesh.points[:, 2]
        >>> mesh.active_scalars_name
        'Z Height'

        """
        return self.active_scalars_info.name

    @active_scalars_name.setter
    def active_scalars_name(self, name: str):
        self.set_active_scalars(name)

    @property
    def points(self) -> pyvista_ndarray:
        """Return a reference to the points as a numpy object.

        Examples
        --------
        Create a mesh and return the points of the mesh as a numpy
        array.

        >>> import pyvista
        >>> cube = pyvista.Cube()
        >>> points = cube.points
        >>> points
        pyvista_ndarray([[-0.5, -0.5, -0.5],
                         [-0.5, -0.5,  0.5],
                         [-0.5,  0.5,  0.5],
                         [-0.5,  0.5, -0.5],
                         [ 0.5, -0.5, -0.5],
                         [ 0.5,  0.5, -0.5],
                         [ 0.5,  0.5,  0.5],
                         [ 0.5, -0.5,  0.5]], dtype=float32)

        Shift these points in the z direction and show that their
        position is reflected in the mesh points.

        >>> points[:, 2] += 1
        >>> cube.points
        pyvista_ndarray([[-0.5, -0.5,  0.5],
                         [-0.5, -0.5,  1.5],
                         [-0.5,  0.5,  1.5],
                         [-0.5,  0.5,  0.5],
                         [ 0.5, -0.5,  0.5],
                         [ 0.5,  0.5,  0.5],
                         [ 0.5,  0.5,  1.5],
                         [ 0.5, -0.5,  1.5]], dtype=float32)

        You can also update the points in-place:

        >>> cube.points[...] = 2 * points
        >>> cube.points
        pyvista_ndarray([[-1., -1.,  1.],
                         [-1., -1.,  3.],
                         [-1.,  1.,  3.],
                         [-1.,  1.,  1.],
                         [ 1., -1.,  1.],
                         [ 1.,  1.,  1.],
                         [ 1.,  1.,  3.],
                         [ 1., -1.,  3.]], dtype=float32)

        """
        _points = self.GetPoints()
        try:
            _points = _points.GetData()
        except AttributeError:
            # create an empty array
            vtk_points = pyvista.vtk_points(np.empty((0, 3)), False)
            self.SetPoints(vtk_points)
            _points = self.GetPoints().GetData()
        return pyvista_ndarray(_points, dataset=self)

    @points.setter
    def points(self, points: Union[VectorArray, NumericArray, _vtk.vtkPoints]):
        pdata = self.GetPoints()
        if isinstance(points, pyvista_ndarray):
            # simply set the underlying data
            if points.VTKObject is not None and pdata is not None:
                pdata.SetData(points.VTKObject)
                pdata.Modified()
                self.Modified()
                return
        # directly set the data if vtk object
        if isinstance(points, _vtk.vtkPoints):
            self.SetPoints(points)
            if pdata is not None:
                pdata.Modified()
            self.Modified()
            return
        # otherwise, wrap and use the array
        points, _ = _coerce_pointslike_arg(points, copy=False)
        vtk_points = pyvista.vtk_points(points, False)
        if not pdata:
            self.SetPoints(vtk_points)
        else:
            pdata.SetData(vtk_points.GetData())
        self.GetPoints().Modified()
        self.Modified()

    @property
    def arrows(self) -> Optional[pyvista.PolyData]:
        """Return a glyph representation of the active vector data as arrows.

        Arrows will be located at the points of the mesh and
        their size will be dependent on the norm of the vector.
        Their direction will be the "direction" of the vector

        Returns
        -------
        pyvista.PolyData
            Active vectors represented as arrows.

        Examples
        --------
        Create a mesh, compute the normals and set them active, and
        plot the active vectors.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh_w_normals = mesh.compute_normals()
        >>> mesh_w_normals.active_vectors_name = 'Normals'
        >>> arrows = mesh_w_normals.arrows
        >>> arrows.plot(show_scalar_bar=False)

        """
        vectors_name = self.active_vectors_name
        if vectors_name is None:
            return

        if self.active_vectors.ndim != 2:  # type: ignore
            raise ValueError('Active vectors are not vectors.')

        scale_name = f'{vectors_name} Magnitude'
        scale = np.linalg.norm(self.active_vectors, axis=1)  # type: ignore
        self.point_data.set_array(scale, scale_name)
        return self.glyph(orient=vectors_name, scale=scale_name)

    @property
    def active_t_coords(self) -> Optional[pyvista_ndarray]:
        """Return or set the active texture coordinates on the points.

        Examples
        --------
        Return the active texture coordinates from the globe example.

        >>> from pyvista import examples
        >>> globe = examples.load_globe()
        >>> globe.active_t_coords
        pyvista_ndarray([[0.        , 0.        ],
                         [0.        , 0.07142857],
                         [0.        , 0.14285714],
                         ...,
                         [1.        , 0.85714286],
                         [1.        , 0.92857143],
                         [1.        , 1.        ]])

        """
        return self.point_data.active_t_coords

    @active_t_coords.setter
    def active_t_coords(self, t_coords: np.ndarray):
        self.point_data.active_t_coords = t_coords  # type: ignore

    @property
    def textures(self) -> Dict[str, _vtk.vtkTexture]:
        """Return a dictionary to hold compatible ``vtk.vtkTexture`` objects.

        When casting back to a VTK dataset or filtering this dataset,
        these textures will not be passed.

        Examples
        --------
        Return the active texture datasets from the globe example.

        >>> from pyvista import examples
        >>> globe = examples.load_globe()
        >>> globe.textures
        {'2k_earth_daymap': ...}

        """
        return self._textures

    def clear_textures(self):
        """Clear the textures from this mesh.

        Examples
        --------
        Clear the texture from the globe example.

        >>> from pyvista import examples
        >>> globe = examples.load_globe()
        >>> globe.textures
        {'2k_earth_daymap': ...}
        >>> globe.clear_textures()
        >>> globe.textures
        {}

        """
        self._textures.clear()

    def _activate_texture(mesh, name: str) -> _vtk.vtkTexture:
        """Grab a texture and update the active texture coordinates.

        This makes sure to not destroy old texture coordinates.

        Parameters
        ----------
        name : str
            The name of the texture and texture coordinates to activate

        Returns
        -------
        vtk.vtkTexture
            The active texture

        """
        if name is True or isinstance(name, int):
            keys = list(mesh.textures.keys())
            # Grab the first name available if True
            idx = 0 if not isinstance(name, int) or name is True else name
            if idx > len(keys):  # is this necessary?
                idx = 0
            try:
                name = keys[idx]
            except IndexError:
                warnings.warn('No textures associated with input mesh.')
                return None
        # Grab the texture object by name
        try:
            texture = mesh.textures[name]
        except KeyError:
            warnings.warn(f'Texture ({name}) not associated with this dataset')
            texture = None
        else:
            # Be sure to reset the tcoords if present
            # Grab old coordinates
            if name in mesh.array_names:
                old_tcoord = mesh.GetPointData().GetTCoords()
                mesh.GetPointData().SetTCoords(mesh.GetPointData().GetAbstractArray(name))
                mesh.GetPointData().AddArray(old_tcoord)
                mesh.Modified()
        return texture

    def set_active_scalars(self, name: Optional[str], preference='cell'):
        """Find the scalars by name and appropriately sets it as active.

        To deactivate any active scalars, pass ``None`` as the ``name``.

        Parameters
        ----------
        name : str, optional
            Name of the scalars array to assign as active.  If
            ``None``, deactivates active scalars for both point and
            cell data.

        preference : str, default: "cell"
            If there are two arrays of the same name associated with
            points or cells, it will prioritize an array matching this
            type.  Can be either ``'cell'`` or ``'point'``.

        Returns
        -------
        pyvista.utilities.helpers.FieldAssociation
            Association of the scalars matching ``name``.

        numpy.ndarray
            An array from the dataset matching ``name``.

        """
        if preference not in ['point', 'cell', FieldAssociation.CELL, FieldAssociation.POINT]:
            raise ValueError('``preference`` must be either "point" or "cell"')
        if name is None:
            self.GetCellData().SetActiveScalars(None)
            self.GetPointData().SetActiveScalars(None)
            return FieldAssociation.NONE, np.array([])
        field = get_array_association(self, name, preference=preference)
        if field == FieldAssociation.NONE:
            if name in self.field_data:
                raise ValueError(f'Data named "{name}" is a field array which cannot be active.')
            else:
                raise KeyError(f'Data named "{name}" does not exist in this dataset.')
        self._last_active_scalars_name = self.active_scalars_info.name
        if field == FieldAssociation.POINT:
            ret = self.GetPointData().SetActiveScalars(name)
        elif field == FieldAssociation.CELL:
            ret = self.GetCellData().SetActiveScalars(name)
        else:
            raise ValueError(f'Data field ({name}) with type ({field}) not usable')

        if ret < 0:
            raise ValueError(
                f'Data field "{name}" with type ({field}) could not be set as the active scalars'
            )

        self._active_scalars_info = ActiveArrayInfo(field, name)

        if field == FieldAssociation.POINT:
            return field, self.point_data.active_scalars
        else:  # must be cell
            return field, self.cell_data.active_scalars

    def set_active_vectors(self, name: Optional[str], preference='point'):
        """Find the vectors by name and appropriately sets it as active.

        To deactivate any active vectors, pass ``None`` as the ``name``.

        Parameters
        ----------
        name : str, optional
            Name of the vectors array to assign as active.

        preference : str, default: "point"
            If there are two arrays of the same name associated with
            points, cells, or field data, it will prioritize an array
            matching this type.  Can be either ``'cell'``,
            ``'field'``, or ``'point'``.

        """
        if name is None:
            self.GetCellData().SetActiveVectors(None)
            self.GetPointData().SetActiveVectors(None)
            field = FieldAssociation.POINT
        else:
            field = get_array_association(self, name, preference=preference)
            if field == FieldAssociation.POINT:
                ret = self.GetPointData().SetActiveVectors(name)
            elif field == FieldAssociation.CELL:
                ret = self.GetCellData().SetActiveVectors(name)
            else:
                raise ValueError(f'Data field ({name}) with type ({field}) not usable')

            if ret < 0:
                raise ValueError(
                    f'Data field ({name}) with type ({field}) could not be set as the active vectors'
                )

        self._active_vectors_info = ActiveArrayInfo(field, name)

    def set_active_tensors(self, name: Optional[str], preference='point'):
        """Find the tensors by name and appropriately sets it as active.

        To deactivate any active tensors, pass ``None`` as the ``name``.

        Parameters
        ----------
        name : str, optional
            Name of the tensors array to assign as active.

        preference : str, default: "point"
            If there are two arrays of the same name associated with
            points, cells, or field data, it will prioritize an array
            matching this type.  Can be either ``'cell'``,
            ``'field'``, or ``'point'``.

        """
        if name is None:
            self.GetCellData().SetActiveTensors(None)
            self.GetPointData().SetActiveTensors(None)
            field = FieldAssociation.POINT
        else:
            field = get_array_association(self, name, preference=preference)
            if field == FieldAssociation.POINT:
                ret = self.GetPointData().SetActiveTensors(name)
            elif field == FieldAssociation.CELL:
                ret = self.GetCellData().SetActiveTensors(name)
            else:
                raise ValueError(f'Data field ({name}) with type ({field}) not usable')

            if ret < 0:
                raise ValueError(
                    f'Data field ({name}) with type ({field}) could not be set as the active tensors'
                )

        self._active_tensors_info = ActiveArrayInfo(field, name)

    def rename_array(self, old_name: str, new_name: str, preference='cell'):
        """Change array name by searching for the array then renaming it.

        Parameters
        ----------
        old_name : str
            Name of the array to rename.

        new_name : str
            Name to rename the array to.

        preference : str, default: "cell"
            If there are two arrays of the same name associated with
            points, cells, or field data, it will prioritize an array
            matching this type.  Can be either ``'cell'``,
            ``'field'``, or ``'point'``.

        Examples
        --------
        Create a cube, assign a point array to the mesh named
        ``'my_array'``, and rename it to ``'my_renamed_array'``.

        >>> import pyvista
        >>> import numpy as np
        >>> cube = pyvista.Cube()
        >>> cube['my_array'] = range(cube.n_points)
        >>> cube.rename_array('my_array', 'my_renamed_array')
        >>> cube['my_renamed_array']
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        """
        field = get_array_association(self, old_name, preference=preference)

        was_active = False
        if self.active_scalars_name == old_name:
            was_active = True
        if field == FieldAssociation.POINT:
            self.point_data[new_name] = self.point_data.pop(old_name)
        elif field == FieldAssociation.CELL:
            self.cell_data[new_name] = self.cell_data.pop(old_name)
        elif field == FieldAssociation.NONE:
            self.field_data[new_name] = self.field_data.pop(old_name)
        else:
            raise KeyError(f'Array with name {old_name} not found.')
        if was_active and field != FieldAssociation.NONE:
            self.set_active_scalars(new_name, preference=field)

    @property
    def active_scalars(self) -> Optional[pyvista_ndarray]:
        """Return the active scalars as an array."""
        field, name = self.active_scalars_info
        if name is not None:
            try:
                if field == FieldAssociation.POINT:
                    return self.point_data[name]
                if field == FieldAssociation.CELL:
                    return self.cell_data[name]
            except KeyError:
                return None
        return None

    @property
    def active_normals(self) -> Optional[pyvista_ndarray]:
        """Return the active normals as an array.

        Returns
        -------
        pyvista_ndarray
            Active normals of this dataset.

        Notes
        -----
        If both point and cell normals exist, this returns point
        normals by default.

        Examples
        --------
        Compute normals on an example sphere mesh and return the
        active normals for the dataset.  Show that this is the same size
        as the number of points.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh = mesh.compute_normals()
        >>> normals = mesh.active_normals
        >>> normals.shape
        (842, 3)
        >>> mesh.n_points
        842
        """
        if self.point_data.active_normals is not None:
            return self.point_data.active_normals
        return self.cell_data.active_normals

    def get_data_range(
        self, arr_var: Optional[Union[str, np.ndarray]] = None, preference='cell'
    ) -> Tuple[Union[float, np.ndarray], Union[float, np.ndarray]]:
        """Get the non-NaN min and max of a named array.

        Parameters
        ----------
        arr_var : str, np.ndarray, optional
            The name of the array to get the range. If ``None``, the
            active scalars is used.

        preference : str, default: "cell"
            When scalars is specified, this is the preferred array type
            to search for in the dataset.  Must be either ``'point'``,
            ``'cell'``, or ``'field'``.

        Returns
        -------
        tuple
            ``(min, max)`` of the named array.

        """
        if arr_var is None:  # use active scalars array
            _, arr_var = self.active_scalars_info
            if arr_var is None:
                return (np.nan, np.nan)

        if isinstance(arr_var, str):
            name = arr_var
            arr = get_array(self, name, preference=preference, err=True)
        else:
            arr = arr_var

        # If array has no tuples return a NaN range
        if arr is None:
            return (np.nan, np.nan)
        if arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
            return (np.nan, np.nan)
        # Use the array range
        return np.nanmin(arr), np.nanmax(arr)

    def rotate_x(
        self, angle: float, point=(0.0, 0.0, 0.0), transform_all_input_vectors=False, inplace=False
    ):
        """Rotate mesh about the x-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        angle : float
            Angle in degrees to rotate about the x-axis.

        point : sequence[float], default: (0.0, 0.0, 0.0)
            Point to rotate about. Defaults to origin.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Rotated dataset.

        Examples
        --------
        Rotate a mesh 30 degrees about the x-axis.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> rot = mesh.rotate_x(30, inplace=False)

        Plot the rotated mesh.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(rot)
        >>> _ = pl.add_mesh(mesh, style='wireframe', line_width=3)
        >>> _ = pl.add_axes_at_origin()
        >>> pl.show()

        """
        check_valid_vector(point, "point")
        t = transformations.axis_angle_rotation((1, 0, 0), angle, point=point, deg=True)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def rotate_y(
        self, angle: float, point=(0.0, 0.0, 0.0), transform_all_input_vectors=False, inplace=False
    ):
        """Rotate mesh about the y-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        angle : float
            Angle in degrees to rotate about the y-axis.

        point : sequence[float], default: (0.0, 0.0, 0.0)
            Point to rotate about.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are transformed. Otherwise, only
            the points, normals and active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Rotated dataset.

        Examples
        --------
        Rotate a cube 30 degrees about the y-axis.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> rot = mesh.rotate_y(30, inplace=False)

        Plot the rotated mesh.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(rot)
        >>> _ = pl.add_mesh(mesh, style='wireframe', line_width=3)
        >>> _ = pl.add_axes_at_origin()
        >>> pl.show()

        """
        check_valid_vector(point, "point")
        t = transformations.axis_angle_rotation((0, 1, 0), angle, point=point, deg=True)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def rotate_z(
        self, angle: float, point=(0.0, 0.0, 0.0), transform_all_input_vectors=False, inplace=False
    ):
        """Rotate mesh about the z-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        angle : float
            Angle in degrees to rotate about the z-axis.

        point : sequence[float], default: (0.0, 0.0, 0.0)
            Point to rotate about.  Defaults to origin.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Rotated dataset.

        Examples
        --------
        Rotate a mesh 30 degrees about the z-axis.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> rot = mesh.rotate_z(30, inplace=False)

        Plot the rotated mesh.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(rot)
        >>> _ = pl.add_mesh(mesh, style='wireframe', line_width=3)
        >>> _ = pl.add_axes_at_origin()
        >>> pl.show()

        """
        check_valid_vector(point, "point")
        t = transformations.axis_angle_rotation((0, 0, 1), angle, point=point, deg=True)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def rotate_vector(
        self,
        vector: Iterable[float],
        angle: float,
        point=(0.0, 0.0, 0.0),
        transform_all_input_vectors=False,
        inplace=False,
    ):
        """Rotate mesh about a vector.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        vector : sequence[float]
            Axis to rotate about.

        angle : float
            Angle in degrees to rotate about the vector.

        point : sequence[float], default: (0.0, 0.0, 0.0)
            Point to rotate about.  Defaults to origin.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Rotated dataset.

        Examples
        --------
        Rotate a mesh 30 degrees about the ``(1, 1, 1)`` axis.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> rot = mesh.rotate_vector((1, 1, 1), 30, inplace=False)

        Plot the rotated mesh.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(rot)
        >>> _ = pl.add_mesh(mesh, style='wireframe', line_width=3)
        >>> _ = pl.add_axes_at_origin()
        >>> pl.show()

        """
        check_valid_vector(vector)
        check_valid_vector(point, "point")
        t = transformations.axis_angle_rotation(vector, angle, point=point, deg=True)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def translate(
        self, xyz: Union[list, tuple, np.ndarray], transform_all_input_vectors=False, inplace=False
    ):
        """Translate the mesh.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        xyz : sequence[float]
            Length 3 sequence of floats.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Translated dataset.

        Examples
        --------
        Create a sphere and translate it by ``(2, 1, 2)``.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.center
        [0.0, 0.0, 0.0]
        >>> trans = mesh.translate((2, 1, 2), inplace=False)
        >>> trans.center
        [2.0, 1.0, 2.0]

        """
        transform = _vtk.vtkTransform()
        transform.Translate(xyz)
        return self.transform(
            transform, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def scale(
        self,
        xyz: Union[Number, list, tuple, np.ndarray],
        transform_all_input_vectors=False,
        inplace=False,
    ):
        """Scale the mesh.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        xyz : float | sequence[float]
            A scalar or length 3 sequence defining the scale factors along x,
            y, and z. If a scalar, the same uniform scale is used along all
            three axes.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are transformed. Otherwise, only
            the points, normals and active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Scaled dataset.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> pl.show_axes()
        >>> _ = pl.show_grid()
        >>> mesh1 = examples.download_teapot()
        >>> _ = pl.add_mesh(mesh1)
        >>> pl.subplot(0, 1)
        >>> pl.show_axes()
        >>> _ = pl.show_grid()
        >>> mesh2 = mesh1.scale([10.0, 10.0, 10.0], inplace=False)
        >>> _ = pl.add_mesh(mesh2)
        >>> pl.show(cpos="xy")

        """
        if isinstance(xyz, (float, int, np.number)):
            xyz = [xyz] * 3

        transform = _vtk.vtkTransform()
        transform.Scale(xyz)
        return self.transform(
            transform, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def flip_x(self, point=None, transform_all_input_vectors=False, inplace=False):
        """Flip mesh about the x-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        point : sequence[float], optional
            Point to rotate about.  Defaults to center of mesh at
            :attr:`center <pyvista.DataSet.center>`.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Flipped dataset.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> pl.show_axes()
        >>> mesh1 = examples.download_teapot()
        >>> _ = pl.add_mesh(mesh1)
        >>> pl.subplot(0, 1)
        >>> pl.show_axes()
        >>> mesh2 = mesh1.flip_x(inplace=False)
        >>> _ = pl.add_mesh(mesh2)
        >>> pl.show(cpos="xy")

        """
        if point is None:
            point = self.center
        check_valid_vector(point, 'point')
        t = transformations.reflection((1, 0, 0), point=point)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def flip_y(self, point=None, transform_all_input_vectors=False, inplace=False):
        """Flip mesh about the y-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        point : sequence[float], optional
            Point to rotate about.  Defaults to center of mesh at
            :attr:`center <pyvista.DataSet.center>`.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Flipped dataset.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> pl.show_axes()
        >>> mesh1 = examples.download_teapot()
        >>> _ = pl.add_mesh(mesh1)
        >>> pl.subplot(0, 1)
        >>> pl.show_axes()
        >>> mesh2 = mesh1.flip_y(inplace=False)
        >>> _ = pl.add_mesh(mesh2)
        >>> pl.show(cpos="xy")

        """
        if point is None:
            point = self.center
        check_valid_vector(point, 'point')
        t = transformations.reflection((0, 1, 0), point=point)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def flip_z(self, point=None, transform_all_input_vectors=False, inplace=False):
        """Flip mesh about the z-axis.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        point : list, optional
            Point to rotate about.  Defaults to center of mesh at
            :attr:`center <pyvista.DataSet.center>`.

        transform_all_input_vectors : bool, optional
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, optional
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Flipped dataset.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> pl.show_axes()
        >>> mesh1 = examples.download_teapot().rotate_x(90, inplace=False)
        >>> _ = pl.add_mesh(mesh1)
        >>> pl.subplot(0, 1)
        >>> pl.show_axes()
        >>> mesh2 = mesh1.flip_z(inplace=False)
        >>> _ = pl.add_mesh(mesh2)
        >>> pl.show(cpos="xz")

        """
        if point is None:
            point = self.center
        check_valid_vector(point, 'point')
        t = transformations.reflection((0, 0, 1), point=point)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def flip_normal(
        self, normal: List[float], point=None, transform_all_input_vectors=False, inplace=False
    ):
        """Flip mesh about the normal.

        .. note::
            See also the notes at :func:`transform()
            <DataSetFilters.transform>` which is used by this filter
            under the hood.

        Parameters
        ----------
        normal : tuple
           Normal vector to flip about.

        point : sequence[float]
            Point to rotate about.  Defaults to center of mesh at
            :attr:`center <pyvista.DataSet.center>`.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are
            transformed. Otherwise, only the points, normals and
            active vectors are transformed.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Dataset flipped about its normal.

        Examples
        --------
        >>> import pyvista
        >>> from pyvista import examples
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.subplot(0, 0)
        >>> pl.show_axes()
        >>> mesh1 = examples.download_teapot()
        >>> _ = pl.add_mesh(mesh1)
        >>> pl.subplot(0, 1)
        >>> pl.show_axes()
        >>> mesh2 = mesh1.flip_normal([1.0, 1.0, 1.0], inplace=False)
        >>> _ = pl.add_mesh(mesh2)
        >>> pl.show(cpos="xy")

        """
        if point is None:
            point = self.center
        check_valid_vector(normal, 'normal')
        check_valid_vector(point, 'point')
        t = transformations.reflection(normal, point=point)
        return self.transform(
            t, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )

    def copy_meta_from(self, ido: DataSet, deep: bool = True):
        """Copy pyvista meta data onto this object from another object.

        Parameters
        ----------
        ido : pyvista.DataSet
            Dataset to copy the metadata from.

        deep : bool, default: True
            Deep or shallow copy.

        """
        self.clear_textures()

        if deep:
            self._association_complex_names = deepcopy(ido._association_complex_names)
            self._association_bitarray_names = deepcopy(ido._association_bitarray_names)
            self._active_scalars_info = ido.active_scalars_info.copy()
            self._active_vectors_info = ido.active_vectors_info.copy()
            self._active_tensors_info = ido.active_tensors_info.copy()
            self._textures = {name: tex.copy() for name, tex in ido.textures.items()}
        else:
            # pass by reference
            self._association_complex_names = ido._association_complex_names
            self._association_bitarray_names = ido._association_bitarray_names
            self._active_scalars_info = ido.active_scalars_info
            self._active_vectors_info = ido.active_vectors_info
            self._active_tensors_info = ido.active_tensors_info
            self._textures = ido.textures

    @property
    def point_data(self) -> DataSetAttributes:
        """Return vtkPointData as DataSetAttributes.

        Examples
        --------
        Add point arrays to a mesh and list the available ``point_data``.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.point_data['my_array'] = np.random.random(mesh.n_points)
        >>> mesh.point_data['my_other_array'] = np.arange(mesh.n_points)
        >>> mesh.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : my_array
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            my_array                float64    (8,)                 SCALARS
            my_other_array          int64      (8,)

        Access an array from ``point_data``.

        >>> mesh.point_data['my_other_array']
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        Or access it directly from the mesh.

        >>> mesh['my_array'].shape
        (8,)

        """
        return DataSetAttributes(
            self.GetPointData(), dataset=self, association=FieldAssociation.POINT
        )

    def clear_point_data(self):
        """Remove all point arrays.

        Examples
        --------
        Clear all point arrays from a mesh.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Sphere()
        >>> mesh.point_data.keys()
        ['Normals']
        >>> mesh.clear_point_data()
        >>> mesh.point_data.keys()
        []

        """
        self.point_data.clear()

    def clear_cell_data(self):
        """Remove all cell arrays."""
        self.cell_data.clear()

    def clear_data(self):
        """Remove all arrays from point/cell/field data.

        Examples
        --------
        Clear all arrays from a mesh.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Sphere()
        >>> mesh.point_data.keys()
        ['Normals']
        >>> mesh.clear_data()
        >>> mesh.point_data.keys()
        []

        """
        self.clear_point_data()
        self.clear_cell_data()
        self.clear_field_data()

    @property
    def cell_data(self) -> DataSetAttributes:
        """Return vtkCellData as DataSetAttributes.

        Examples
        --------
        Add cell arrays to a mesh and list the available ``cell_data``.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.cell_data['my_array'] = np.random.random(mesh.n_cells)
        >>> mesh.cell_data['my_other_array'] = np.arange(mesh.n_cells)
        >>> mesh.cell_data
        pyvista DataSetAttributes
        Association     : CELL
        Active Scalars  : my_array
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            my_array                float64    (6,)                 SCALARS
            my_other_array          int64      (6,)

        Access an array from ``cell_data``.

        >>> mesh.cell_data['my_other_array']
        pyvista_ndarray([0, 1, 2, 3, 4, 5])

        Or access it directly from the mesh.

        >>> mesh['my_array'].shape
        (6,)

        """
        return DataSetAttributes(
            self.GetCellData(), dataset=self, association=FieldAssociation.CELL
        )

    @property
    def n_points(self) -> int:
        """Return the number of points in the entire dataset.

        Examples
        --------
        Create a mesh and return the number of points in the
        mesh.

        >>> import pyvista
        >>> cube = pyvista.Cube()
        >>> cube.n_points
        8

        """
        return self.GetNumberOfPoints()

    @property
    def n_cells(self) -> int:
        """Return the number of cells in the entire dataset.

        Notes
        -----
        This is identical to :attr:`n_faces <pyvista.PolyData.n_faces>`
        in :class:`pyvista.PolyData`.

        Examples
        --------
        Create a mesh and return the number of cells in the
        mesh.

        >>> import pyvista
        >>> cube = pyvista.Cube()
        >>> cube.n_cells
        6

        """
        return self.GetNumberOfCells()

    @property
    def number_of_points(self) -> int:  # pragma: no cover
        """Return the number of points."""
        return self.GetNumberOfPoints()

    @property
    def number_of_cells(self) -> int:  # pragma: no cover
        """Return the number of cells."""
        return self.GetNumberOfCells()

    @property
    def bounds(self) -> BoundsLike:
        """Return the bounding box of this dataset.

        The form is: ``(xmin, xmax, ymin, ymax, zmin, zmax)``.

        Examples
        --------
        Create a cube and return the bounds of the mesh.

        >>> import pyvista
        >>> cube = pyvista.Cube()
        >>> cube.bounds
        (-0.5, 0.5, -0.5, 0.5, -0.5, 0.5)

        """
        return cast(BoundsLike, self.GetBounds())

    @property
    def length(self) -> float:
        """Return the length of the diagonal of the bounding box.

        Examples
        --------
        Get the length of the bounding box of a cube.  This should
        match ``3**(1/2)`` since it is the diagonal of a cube that is
        ``1 x 1 x 1``.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.length
        1.7320508075688772

        """
        return self.GetLength()

    @property
    def center(self) -> Vector:
        """Return the center of the bounding box.

        Examples
        --------
        Get the center of a mesh.

        >>> import pyvista
        >>> mesh = pyvista.Sphere(center=(1, 2, 0))
        >>> mesh.center
        [1.0, 2.0, 0.0]

        """
        return list(self.GetCenter())

    @property
    def volume(self) -> float:
        """Return the mesh volume.

        This will return 0 for meshes with 2D cells.

        Returns
        -------
        float
            Total volume of the mesh.

        Examples
        --------
        Get the volume of a cube of size 4x4x4.
        Note that there are 5 points in each direction.

        >>> import pyvista as pv
        >>> mesh = pv.UniformGrid(dimensions=(5, 5, 5))
        >>> mesh.volume
        64.0

        A mesh with 2D cells has no volume.

        >>> mesh = pv.UniformGrid(dimensions=(5, 5, 1))
        >>> mesh.volume
        0.0

        :class:`pyvista.PolyData` is special as a 2D surface can
        enclose a 3D volume.

        >>> mesh = pv.Sphere()
        >>> mesh.volume
        0.51825

        """
        sizes = self.compute_cell_sizes(length=False, area=False, volume=True)
        return sizes.cell_data['Volume'].sum()

    @property
    def area(self) -> float:
        """Return the mesh area if 2D.

        This will return 0 for meshes with 3D cells.

        Returns
        -------
        float
            Total area of the mesh.

        Examples
        --------
        Get the area of a square of size 2x2.
        Note 5 points in each direction.

        >>> import pyvista as pv
        >>> mesh = pv.UniformGrid(dimensions=(5, 5, 1))
        >>> mesh.area
        16.0

        A mesh with 3D cells does not have an area.  To get
        the outer surface area, first extract the surface using
        :func:`pyvista.DataSetFilters.extract_surface`.

        >>> mesh = pv.UniformGrid(dimensions=(5, 5, 5))
        >>> mesh.area
        0.0

        Get the area of a sphere.

        >>> mesh = pv.Sphere()
        >>> mesh.volume
        0.51825

        """
        sizes = self.compute_cell_sizes(length=False, area=True, volume=False)
        return sizes.cell_data['Area'].sum()

    def get_array(
        self, name: str, preference: Literal['cell', 'point', 'field'] = 'cell'
    ) -> pyvista.pyvista_ndarray:
        """Search both point, cell and field data for an array.

        Parameters
        ----------
        name : str
            Name of the array.

        preference : str, default: "cell"
            When scalars is specified, this is the preferred array
            type to search for in the dataset.  Must be either
            ``'point'``, ``'cell'``, or ``'field'``.

        Returns
        -------
        pyvista.pyvista_ndarray
            Requested array.

        Examples
        --------
        Create a DataSet with a variety of arrays.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.point_data['point-data'] = range(mesh.n_points)
        >>> mesh.cell_data['cell-data'] = range(mesh.n_cells)
        >>> mesh.field_data['field-data'] = ['a', 'b', 'c']
        >>> mesh.array_names
        ['point-data', 'field-data', 'cell-data']

        Get the point data array.

        >>> mesh.get_array('point-data')
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        Get the cell data array.

        >>> mesh.get_array('cell-data')
        pyvista_ndarray([0, 1, 2, 3, 4, 5])

        Get the field data array.

        >>> mesh.get_array('field-data')
        pyvista_ndarray(['a', 'b', 'c'], dtype='<U1')

        """
        arr = get_array(self, name, preference=preference, err=True)
        if arr is None:  # pragma: no cover
            raise RuntimeError  # this should never be reached with err=True
        return arr

    def get_array_association(
        self, name: str, preference: Literal['cell', 'point', 'field'] = 'cell'
    ) -> FieldAssociation:
        """Get the association of an array.

        Parameters
        ----------
        name : str
            Name of the array.

        preference : str, default: "cell"
            When ``name`` is specified, this is the preferred array
            association to search for in the dataset.  Must be either
            ``'point'``, ``'cell'``, or ``'field'``.

        Returns
        -------
        pyvista.utilities.helpers.FieldAssociation
            Field association of the array.

        Examples
        --------
        Create a DataSet with a variety of arrays.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.point_data['point-data'] = range(mesh.n_points)
        >>> mesh.cell_data['cell-data'] = range(mesh.n_cells)
        >>> mesh.field_data['field-data'] = ['a', 'b', 'c']
        >>> mesh.array_names
        ['point-data', 'field-data', 'cell-data']

        Get the point data array association.

        >>> mesh.get_array_association('point-data')
        <FieldAssociation.POINT: 0>

        Get the cell data array association.

        >>> mesh.get_array_association('cell-data')
        <FieldAssociation.CELL: 1>

        Get the field data array association.

        >>> mesh.get_array_association('field-data')
        <FieldAssociation.NONE: 2>

        """
        return get_array_association(self, name, preference=preference, err=True)

    def __getitem__(self, index: Union[Iterable, str]) -> np.ndarray:
        """Search both point, cell, and field data for an array."""
        if isinstance(index, collections.abc.Iterable) and not isinstance(index, str):
            name, preference = tuple(index)
        elif isinstance(index, str):
            name = index
            preference = 'cell'
        else:
            raise KeyError(
                f'Index ({index}) not understood.'
                ' Index must be a string name or a tuple of string name and string preference.'
            )
        return self.get_array(name, preference=preference)

    def _ipython_key_completions_(self) -> List[str]:
        return self.array_names

    def __setitem__(self, name: str, scalars: Union[np.ndarray, collections.abc.Sequence]):
        """Add/set an array in the point_data, or cell_data accordingly.

        It depends on the array's length, or specified mode.

        """
        # First check points - think of case with vertex cells
        #   there would be the same number of cells as points but we'd want
        #   the data to be on the nodes.
        if scalars is None:
            raise TypeError('Empty array unable to be added.')
        if not isinstance(scalars, np.ndarray):
            scalars = np.array(scalars)
        # Now check array size to determine which field to place array
        if scalars.shape[0] == self.n_points:
            self.point_data[name] = scalars
        elif scalars.shape[0] == self.n_cells:
            self.cell_data[name] = scalars
        else:
            # Field data must be set explicitly as it could be a point of
            # confusion for new users
            raise_not_matching(scalars, self)
        return

    @property
    def n_arrays(self) -> int:
        """Return the number of arrays present in the dataset."""
        n = self.GetPointData().GetNumberOfArrays()
        n += self.GetCellData().GetNumberOfArrays()
        n += self.GetFieldData().GetNumberOfArrays()
        return n

    @property
    def array_names(self) -> List[str]:
        """Return a list of array names for the dataset.

        This makes sure to put the active scalars' name first in the list.

        Examples
        --------
        Return the array names for a mesh.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.point_data['my_array'] = range(mesh.n_points)
        >>> mesh.array_names
        ['my_array', 'Normals']

        """
        names = []
        names.extend(self.field_data.keys())
        names.extend(self.point_data.keys())
        names.extend(self.cell_data.keys())
        try:
            names.remove(self.active_scalars_name)
            names.insert(0, self.active_scalars_name)
        except ValueError:
            pass
        return names

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = []
        attrs.append(("N Cells", self.GetNumberOfCells(), "{}"))
        attrs.append(("N Points", self.GetNumberOfPoints(), "{}"))
        if isinstance(self, pyvista.PolyData):
            attrs.append(("N Strips", self.n_strips, "{}"))
        bds = self.bounds
        fmt = f"{pyvista.FLOAT_FORMAT}, {pyvista.FLOAT_FORMAT}"
        attrs.append(("X Bounds", (bds[0], bds[1]), fmt))
        attrs.append(("Y Bounds", (bds[2], bds[3]), fmt))
        attrs.append(("Z Bounds", (bds[4], bds[5]), fmt))
        # if self.n_cells <= pyvista.REPR_VOLUME_MAX_CELLS and self.n_cells > 0:
        #     attrs.append(("Volume", (self.volume), pyvista.FLOAT_FORMAT))
        return attrs

    def _repr_html_(self) -> str:
        """Return a pretty representation for Jupyter notebooks.

        It includes header details and information about all arrays.

        """
        fmt = ""
        if self.n_arrays > 0:
            fmt += "<table>"
            fmt += "<tr><th>Header</th><th>Data Arrays</th></tr>"
            fmt += "<tr><td>"
        # Get the header info
        fmt += self.head(display=False, html=True)
        # Fill out arrays
        if self.n_arrays > 0:
            fmt += "</td><td>"
            fmt += "\n"
            fmt += "<table>\n"
            titles = ["Name", "Field", "Type", "N Comp", "Min", "Max"]
            fmt += "<tr>" + "".join([f"<th>{t}</th>" for t in titles]) + "</tr>\n"
            row = "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n"
            row = "<tr>" + "".join(["<td>{}</td>" for i in range(len(titles))]) + "</tr>\n"

            def format_array(name, arr, field):
                """Format array information for printing (internal helper)."""
                dl, dh = self.get_data_range(arr)
                dl = pyvista.FLOAT_FORMAT.format(dl)
                dh = pyvista.FLOAT_FORMAT.format(dh)
                if name == self.active_scalars_info.name:
                    name = f'<b>{name}</b>'
                if arr.ndim > 1:
                    ncomp = arr.shape[1]
                else:
                    ncomp = 1
                return row.format(name, field, arr.dtype, ncomp, dl, dh)

            for key, arr in self.point_data.items():
                fmt += format_array(key, arr, 'Points')
            for key, arr in self.cell_data.items():
                fmt += format_array(key, arr, 'Cells')
            for key, arr in self.field_data.items():
                fmt += format_array(key, arr, 'Fields')

            fmt += "</table>\n"
            fmt += "\n"
            fmt += "</td></tr> </table>"
        return fmt

    def __repr__(self) -> str:
        """Return the object representation."""
        return self.head(display=False, html=False)

    def __str__(self) -> str:
        """Return the object string representation."""
        return self.head(display=False, html=False)

    def copy_from(self, mesh: _vtk.vtkDataSet, deep: bool = True):
        """Overwrite this dataset inplace with the new dataset's geometries and data.

        Parameters
        ----------
        mesh : vtk.vtkDataSet
            The overwriting mesh.

        deep : bool, default: True
            Whether to perform a deep or shallow copy.

        Examples
        --------
        Create two meshes and overwrite ``mesh_a`` with ``mesh_b``.
        Show that ``mesh_a`` is equal to ``mesh_b``.

        >>> import pyvista
        >>> mesh_a = pyvista.Sphere()
        >>> mesh_b = pyvista.Cube()
        >>> mesh_a.copy_from(mesh_b)
        >>> mesh_a == mesh_b
        True

        """
        # Allow child classes to overwrite parent classes
        if not isinstance(self, type(mesh)):
            raise TypeError(
                f'The Input DataSet type {type(mesh)} must be '
                f'compatible with the one being overwritten {type(self)}'
            )
        if deep:
            self.deep_copy(mesh)
        else:
            self.shallow_copy(mesh)
        if is_pyvista_dataset(mesh):
            self.copy_meta_from(mesh, deep=deep)

    def overwrite(self, mesh: _vtk.vtkDataSet):
        """Overwrite this dataset inplace with the new dataset's geometries and data.

        .. deprecated:: 0.37.0
            Use :func:`DataSet.copy_from` instead.

        Parameters
        ----------
        mesh : vtk.vtkDataSet
            The overwriting mesh.

        """
        # Deprecated on v0.37.0, estimated removal on v0.40.0
        warnings.warn(
            "Use of `DataSet.overwrite` is deprecated. Use `DataSet.copy_from` instead.",
            PyVistaDeprecationWarning,
        )
        self.copy_from(mesh)

    def cast_to_unstructured_grid(self) -> pyvista.UnstructuredGrid:
        """Get a new representation of this object as a :class:`pyvista.UnstructuredGrid`.

        Returns
        -------
        pyvista.UnstructuredGrid
            Dataset cast into a :class:`pyvista.UnstructuredGrid`.

        Examples
        --------
        Cast a :class:`pyvista.PolyData` to a
        :class:`pyvista.UnstructuredGrid`.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> type(mesh)
        <class 'pyvista.core.pointset.PolyData'>
        >>> grid = mesh.cast_to_unstructured_grid()
        >>> type(grid)
        <class 'pyvista.core.pointset.UnstructuredGrid'>

        """
        alg = _vtk.vtkAppendFilter()
        alg.AddInputData(self)
        alg.Update()
        return _get_output(alg)

    def cast_to_pointset(self, pass_cell_data: bool = False) -> pyvista.PointSet:
        """Extract the points of this dataset and return a :class:`pyvista.PointSet`.

        Parameters
        ----------
        pass_cell_data : bool, default: False
            Run the :func:`cell_data_to_point_data()
            <pyvista.DataSetFilters.cell_data_to_point_data>` filter and pass
            cell data fields to the new pointset.

        Returns
        -------
        pyvista.PointSet
            Dataset cast into a :class:`pyvista.PointSet`.

        Notes
        -----
        This will produce a deep copy of the points and point/cell data of
        the original mesh.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Wavelet()
        >>> pointset = mesh.cast_to_pointset()
        >>> type(pointset)
        <class 'pyvista.core.pointset.PointSet'>

        """
        pset = pyvista.PointSet()
        pset.points = self.points.copy()
        if pass_cell_data:
            self = self.cell_data_to_point_data()
        pset.GetPointData().DeepCopy(self.GetPointData())
        pset.active_scalars_name = self.active_scalars_name
        return pset

    def cast_to_poly_points(self, pass_cell_data: bool = False) -> pyvista.PolyData:
        """Extract the points of this dataset and return a :class:`pyvista.PolyData`.

        Parameters
        ----------
        pass_cell_data : bool, default: False
            Run the :func:`cell_data_to_point_data()
            <pyvista.DataSetFilters.cell_data_to_point_data>` filter and pass
            cell data fields to the new pointset.

        Returns
        -------
        pyvista.PolyData
            Dataset cast into a :class:`pyvista.PolyData`.

        Notes
        -----
        This will produce a deep copy of the points and point/cell data of
        the original mesh.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_uniform()
        >>> points = mesh.cast_to_poly_points(pass_cell_data=True)
        >>> type(points)
        <class 'pyvista.core.pointset.PolyData'>
        >>> points.n_arrays
        2
        >>> points.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : Spatial Point Data
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            Spatial Point Data      float64    (1000,)              SCALARS
        >>> points.cell_data
        pyvista DataSetAttributes
        Association     : CELL
        Active Scalars  : None
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            Spatial Cell Data       float64    (1000,)

        """
        pset = pyvista.PolyData(self.points.copy())
        if pass_cell_data:
            cell_data = self.copy()
            cell_data.clear_point_data()
            cell_data = cell_data.cell_data_to_point_data()
            pset.GetCellData().DeepCopy(cell_data.GetPointData())
        pset.GetPointData().DeepCopy(self.GetPointData())
        pset.active_scalars_name = self.active_scalars_name
        return pset

    def find_closest_point(self, point: Iterable[float], n=1) -> int:
        """Find index of closest point in this mesh to the given point.

        If wanting to query many points, use a KDTree with scipy or another
        library as those implementations will be easier to work with.

        See: https://github.com/pyvista/pyvista-support/issues/107

        Parameters
        ----------
        point : sequence[float]
            Length 3 coordinate of the point to query.

        n : int, optional
            If greater than ``1``, returns the indices of the ``n`` closest
            points.

        Returns
        -------
        int
            The index of the point in this mesh that is closest to the given point.

        See Also
        --------
        DataSet.find_closest_cell
        DataSet.find_containing_cell
        DataSet.find_cells_along_line
        DataSet.find_cells_within_bounds

        Examples
        --------
        Find the index of the closest point to ``(0, 1, 0)``.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> index = mesh.find_closest_point((0, 1, 0))
        >>> index
        212

        Get the coordinate of that point.

        >>> mesh.points[index]
        pyvista_ndarray([-0.05218758,  0.49653167,  0.02706946], dtype=float32)

        """
        if not isinstance(point, (np.ndarray, collections.abc.Sequence)) or len(point) != 3:
            raise TypeError("Given point must be a length three sequence.")
        if not isinstance(n, int):
            raise TypeError("`n` must be a positive integer.")
        if n < 1:
            raise ValueError("`n` must be a positive integer.")

        locator = _vtk.vtkPointLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()
        if n > 1:
            id_list = _vtk.vtkIdList()
            locator.FindClosestNPoints(n, point, id_list)
            return vtk_id_list_to_array(id_list)
        return locator.FindClosestPoint(point)

    def find_closest_cell(
        self,
        point: Union[VectorArray, NumericArray],
        return_closest_point: bool = False,
    ) -> Union[int, np.ndarray, Tuple[Union[int, np.ndarray], np.ndarray]]:
        """Find index of closest cell in this mesh to the given point.

        Parameters
        ----------
        point : array_like[float]
            Coordinates of point to query (length 3) or a
            :class:`numpy.ndarray` of ``n`` points with shape ``(n, 3)``.

        return_closest_point : bool, default: False
            If ``True``, the closest point within a mesh cell to that point is
            returned.  This is not necessarily the closest nodal point on the
            mesh.  Default is ``False``.

        Returns
        -------
        int or numpy.ndarray
            Index or indices of the cell in this mesh that is/are closest
            to the given point(s).

            .. versionchanged:: 0.35.0
               Inputs of shape ``(1, 3)`` now return a :class:`numpy.ndarray`
               of shape ``(1,)``.

        numpy.ndarray
            Point or points inside a cell of the mesh that is/are closest
            to the given point(s).  Only returned if
            ``return_closest_point=True``.

            .. versionchanged:: 0.35.0
               Inputs of shape ``(1, 3)`` now return a :class:`numpy.ndarray`
               of the same shape.

        Warnings
        --------
        This method may still return a valid cell index even if the point
        contains a value like ``numpy.inf`` or ``numpy.nan``.

        See Also
        --------
        DataSet.find_closest_point
        DataSet.find_containing_cell
        DataSet.find_cells_along_line
        DataSet.find_cells_within_bounds

        Examples
        --------
        Find nearest cell on a sphere centered on the
        origin to the point ``[0.1, 0.2, 0.3]``.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> point = [0.1, 0.2, 0.3]
        >>> index = mesh.find_closest_cell(point)
        >>> index
        591

        Make sure that this cell indeed is the closest to
        ``[0.1, 0.2, 0.3]``.

        >>> import numpy as np
        >>> cell_centers = mesh.cell_centers()
        >>> relative_position = cell_centers.points - point
        >>> distance = np.linalg.norm(relative_position, axis=1)
        >>> np.argmin(distance)
        591

        Find the nearest cells to several random points that
        are centered on the origin.

        >>> points = 2 * np.random.random((5000, 3)) - 1
        >>> indices = mesh.find_closest_cell(points)
        >>> indices.shape
        (5000,)

        For the closest cell, find the point inside the cell that is
        closest to the supplied point.  The rectangle is a unit square
        with 1 cell and 4 nodal points at the corners in the plane with
        ``z`` normal and ``z=0``.  The closest point inside the cell is
        not usually at a nodal point.

        >>> unit_square = pyvista.Rectangle()
        >>> index, closest_point = unit_square.find_closest_cell(
        ...     [0.25, 0.25, 0.5], return_closest_point=True
        ... )
        >>> closest_point
        array([0.25, 0.25, 0.  ])

        But, the closest point can be a nodal point, although the index of
        that point is not returned.  If the closest nodal point by index is
        desired, see :func:`DataSet.find_closest_point`.

        >>> index, closest_point = unit_square.find_closest_cell(
        ...     [1.0, 1.0, 0.5], return_closest_point=True
        ... )
        >>> closest_point
        array([1., 1., 0.])

        """
        point, singular = _coerce_pointslike_arg(point, copy=False)

        locator = _vtk.vtkCellLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()

        cell = _vtk.vtkGenericCell()

        closest_cells: List[int] = []
        closest_points: List[List[float]] = []

        for node in point:
            closest_point = [0.0, 0.0, 0.0]
            cell_id = _vtk.mutable(0)
            sub_id = _vtk.mutable(0)
            dist2 = _vtk.mutable(0.0)

            locator.FindClosestPoint(node, closest_point, cell, cell_id, sub_id, dist2)
            closest_cells.append(int(cell_id))
            closest_points.append(closest_point)

        out_cells: Union[int, np.ndarray] = (
            closest_cells[0] if singular else np.array(closest_cells)
        )
        out_points = np.array(closest_points[0]) if singular else np.array(closest_points)

        if return_closest_point:
            return out_cells, out_points
        return out_cells

    def find_containing_cell(
        self, point: Union[VectorArray, NumericArray]
    ) -> Union[int, np.ndarray]:
        """Find index of a cell that contains the given point.

        Parameters
        ----------
        point : array_like[float]
            Coordinates of point to query (length 3) or a
            :class:`numpy.ndarray` of ``n`` points with shape ``(n, 3)``.

        Returns
        -------
        int or numpy.ndarray
            Index or indices of the cell in this mesh that contains
            the given point.

            .. versionchanged:: 0.35.0
               Inputs of shape ``(1, 3)`` now return a :class:`numpy.ndarray`
               of shape ``(1,)``.

        See Also
        --------
        DataSet.find_closest_point
        DataSet.find_closest_cell
        DataSet.find_cells_along_line
        DataSet.find_cells_within_bounds

        Examples
        --------
        A unit square with 16 equal sized cells is created and a cell
        containing the point ``[0.3, 0.3, 0.0]`` is found.

        >>> import pyvista
        >>> mesh = pyvista.UniformGrid(
        ...     dimensions=[5, 5, 1], spacing=[1 / 4, 1 / 4, 0]
        ... )
        >>> mesh
        UniformGrid...
        >>> mesh.find_containing_cell([0.3, 0.3, 0.0])
        5

        A point outside the mesh domain will return ``-1``.

        >>> mesh.find_containing_cell([0.3, 0.3, 1.0])
        -1

        Find the cells that contain 1000 random points inside the mesh.

        >>> import numpy as np
        >>> points = np.random.random((1000, 3))
        >>> indices = mesh.find_containing_cell(points)
        >>> indices.shape
        (1000,)

        """
        point, singular = _coerce_pointslike_arg(point, copy=False)

        locator = _vtk.vtkCellLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()

        containing_cells = [locator.FindCell(node) for node in point]
        return containing_cells[0] if singular else np.array(containing_cells)

    def find_cells_along_line(
        self,
        pointa: Iterable[float],
        pointb: Iterable[float],
        tolerance=0.0,
    ) -> np.ndarray:
        """Find the index of cells whose bounds intersect a line.

        Line is defined from ``pointa`` to ``pointb``.

        Parameters
        ----------
        pointa : sequence[float]
            Length 3 coordinate of the start of the line.

        pointb : sequence[float]
            Length 3 coordinate of the end of the line.

        tolerance : float, default: 0.0
            The absolute tolerance to use to find cells along line.

        Returns
        -------
        numpy.ndarray
            Index or indices of the cell(s) whose bounds intersect
            the line.

        Warnings
        --------
        This method returns cells whose bounds intersect the line.
        This means that the line may not intersect the cell itself.
        To obtain cells that intersect the line, use
        :func:`pyvista.DataSet.find_cells_intersecting_line`.

        See Also
        --------
        DataSet.find_closest_point
        DataSet.find_closest_cell
        DataSet.find_containing_cell
        DataSet.find_cells_within_bounds
        DataSet.find_cells_intersecting_line

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.find_cells_along_line([0.0, 0, 0], [1.0, 0, 0])
        array([842, 843, 896, 897])

        """
        if np.array(pointa).size != 3:
            raise TypeError("Point A must be a length three tuple of floats.")
        if np.array(pointb).size != 3:
            raise TypeError("Point B must be a length three tuple of floats.")
        locator = _vtk.vtkCellLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()
        id_list = _vtk.vtkIdList()
        locator.FindCellsAlongLine(pointa, pointb, tolerance, id_list)
        return vtk_id_list_to_array(id_list)

    def find_cells_intersecting_line(
        self,
        pointa: Iterable[float],
        pointb: Iterable[float],
        tolerance=0.0,
    ) -> np.ndarray:
        """Find the index of cells that intersect a line.

        Line is defined from ``pointa`` to ``pointb``.  This
        method requires vtk version >=9.2.0.

        Parameters
        ----------
        pointa : sequence[float]
            Length 3 coordinate of the start of the line.

        pointb : sequence[float]
            Length 3 coordinate of the end of the line.

        tolerance : float, default: 0.0
            The absolute tolerance to use to find cells along line.

        Returns
        -------
        numpy.ndarray
            Index or indices of the cell(s) that intersect
            the line.

        See Also
        --------
        DataSet.find_closest_point
        DataSet.find_closest_cell
        DataSet.find_containing_cell
        DataSet.find_cells_within_bounds
        DataSet.find_cells_along_line

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.find_cells_intersecting_line([0.0, 0, 0], [1.0, 0, 0])
        array([896])

        """
        if pyvista.vtk_version_info < (9, 2, 0):
            raise VTKVersionError("pyvista.PointSet requires VTK >= 9.2.0")

        if np.array(pointa).size != 3:
            raise TypeError("Point A must be a length three tuple of floats.")
        if np.array(pointb).size != 3:
            raise TypeError("Point B must be a length three tuple of floats.")
        locator = _vtk.vtkCellLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()
        id_list = _vtk.vtkIdList()
        points = _vtk.vtkPoints()
        cell = _vtk.vtkGenericCell()
        locator.IntersectWithLine(pointa, pointb, tolerance, points, id_list, cell)
        return vtk_id_list_to_array(id_list)

    def find_cells_within_bounds(self, bounds: Iterable[float]) -> np.ndarray:
        """Find the index of cells in this mesh within bounds.

        Parameters
        ----------
        bounds : sequence[float]
            Bounding box. The form is: ``[xmin, xmax, ymin, ymax, zmin, zmax]``.

        Returns
        -------
        numpy.ndarray
            Index or indices of the cell in this mesh that are closest
            to the given point.

        See Also
        --------
        DataSet.find_closest_point
        DataSet.find_closest_cell
        DataSet.find_containing_cell
        DataSet.find_cells_along_line

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> index = mesh.find_cells_within_bounds(
        ...     [-2.0, 2.0, -2.0, 2.0, -2.0, 2.0]
        ... )

        """
        if np.array(bounds).size != 6:
            raise TypeError("Bounds must be a length three tuple of floats.")
        locator = _vtk.vtkCellTreeLocator()
        locator.SetDataSet(self)
        locator.BuildLocator()
        id_list = _vtk.vtkIdList()
        locator.FindCellsWithinBounds(list(bounds), id_list)
        return vtk_id_list_to_array(id_list)

    def get_cell(self, index: int) -> pyvista.Cell:
        """Return a :class:`pyvista.Cell` object.

        Parameters
        ----------
        index : int
            Cell ID.

        Returns
        -------
        pyvista.Cell
            The i-th pyvista.Cell.

        Notes
        -----
        Cells returned from this method are deep copies of the original
        cells. Changing properties (for example, ``points``) will not affect
        the dataset they originated from.

        Examples
        --------
        Get the 0-th cell.

        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()
        >>> cell = mesh.get_cell(0)
        >>> cell
        Cell ...

        Get the point ids of the first cell

        >>> cell.point_ids
        [0, 1, 2]

        Get the point coordinates of the first cell

        >>> cell.points
        array([[897.0,  48.8,  82.3],
               [906.6,  48.8,  80.7],
               [907.5,  55.5,  83.7]])

        For the first cell, get the points associated with the first edge

        >>> cell.edges[0].point_ids
        [0, 1]

        For a Tetrahedron, get the point ids of the last face

        >>> mesh = examples.cells.Tetrahedron()
        >>> cell = mesh.get_cell(0)
        >>> cell.faces[-1].point_ids
        [0, 2, 1]

        """
        # must check upper bounds, otherwise segfaults (on Linux, 9.2)
        if index + 1 > self.n_cells:
            raise IndexError(f'Invalid index {index} for a dataset with {self.n_cells} cells.')

        # Note: we have to use vtkGenericCell here since
        # GetCell(vtkIdType cellId, vtkGenericCell* cell) is thread-safe,
        # while GetCell(vtkIdType cellId) is not.
        cell = pyvista.Cell()
        self.GetCell(index, cell)
        cell.SetCellType(self.GetCellType(index))
        return cell

    @property
    def cell(self) -> Iterator[pyvista.Cell]:
        """A generator that provides an easy way to loop over all cells.

        To access a single cell, use :func:`pyvista.DataSet.get_cell`.

        .. versionchanged:: 0.39.0
            Now returns a generator instead of a list.
            Use ``get_cell(i)`` instead of ``cell[i]``.

        Yields
        ------
        pyvista.Cell

        See Also
        --------
        pyvista.DataSet.get_cell

        Examples
        --------
        Loop over the cells

        >>> import pyvista as pv
        >>> # Create a grid with 9 points and 4 cells
        >>> mesh = pv.UniformGrid(dimensions=(3, 3, 1))
        >>> for cell in mesh.cell:  # doctest: +SKIP
        ...     cell
        ...
        """
        for i in range(self.n_cells):
            yield self.get_cell(i)

    def cell_n_points(self, ind: int) -> int:
        """Return the number of points in a cell.

        .. deprecated:: 0.38.0
            Use :attr:`pyvista.Cell.n_points` instead.

        Parameters
        ----------
        ind : int
            Cell ID.

        Returns
        -------
        int
            Number of points in the cell.

        """
        # deprecated 0.38.0, convert to error in 0.41.0, remove 0.42.0
        warnings.warn(
            '`cell_n_points` is deprecated. Use `get_cell(i).n_points` instead',
            PyVistaDeprecationWarning,
        )
        return self.get_cell(ind).n_points

    def cell_points(self, ind: int) -> np.ndarray:
        """Return the points in a cell.

        ..  deprecated:: 0.38.0
            Use :attr:`pyvista.Cell.points` instead.

        Parameters
        ----------
        ind : int
            Cell ID.

        Returns
        -------
        numpy.ndarray
            An array of floats with shape (number of points, 3) containing the coordinates of the
            cell corners.

        """
        # deprecated 0.38.0, convert to error in 0.41.0, remove 0.42.0
        warnings.warn(
            '`cell_points` is deprecated. Use `get_cell(i).points` instead',
            PyVistaDeprecationWarning,
        )
        return self.get_cell(ind).points

    def cell_bounds(self, ind: int) -> BoundsLike:
        """Return the bounding box of a cell.

        ..  deprecated:: 0.38.0
            Use :attr:`pyvista.Cell.bounds` instead.

        Parameters
        ----------
        ind : int
            Cell ID.

        Returns
        -------
        tuple[float, float, float]
            The limits of the cell in the X, Y and Z directions respectively.

        """
        # deprecated 0.38.0, convert to error in 0.41.0, remove 0.42.0
        warnings.warn(
            '`cell_bounds` is deprecated. Use `get_cell(i).bounds` instead',
            PyVistaDeprecationWarning,
        )
        return self.get_cell(ind).bounds

    def cell_type(self, ind: int) -> int:
        """Return the type of a cell.

        .. deprecated:: 0.38.0
            You can use :attr:`pyvista.Cell.type` instead.

        Parameters
        ----------
        ind : int
            Cell type ID.

        Returns
        -------
        int
            VTK cell type. See `vtkCellType.h
            <https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html>`_ .

        """
        # deprecated 0.38.0, convert to error in 0.41.0, remove 0.42.0
        warnings.warn(
            '`cell_type` is deprecated. Use `get_cell(i).type` instead', PyVistaDeprecationWarning
        )
        return self.get_cell(ind).type

    def cell_point_ids(self, ind: int) -> List[int]:
        """Return the point ids in a cell.

        .. deprecated:: 0.38.0
            You can use :attr:`pyvista.Cell.point_ids` instead.

        Parameters
        ----------
        ind : int
            Cell ID.

        Returns
        -------
        list[int]
            Point Ids that are associated with the cell.

        """
        # deprecated 0.38.0, convert to error in 0.41.0, remove 0.42.0
        warnings.warn(
            '`cell_point_ids` is deprecated. Use `get_cell(i).point_ids` instead',
            PyVistaDeprecationWarning,
        )
        return self.get_cell(ind).point_ids

    def cell_neighbors(self, ind: int, connections: str = "points") -> List[int]:
        """Get the cell neighbors of the ind-th cell.

        Concrete implementation of vtkDataSet's `GetCellNeighbors
        <https://vtk.org/doc/nightly/html/classvtkDataSet.html#ae1ba413c15802ef50d9b1955a66521e4>`_.

        Parameters
        ----------
        ind : int
            Cell ID.

        connections : str, default: "points"
            Describe how the neighbor cell(s) must be connected to the current
            cell to be considered as a neighbor.
            Can be either ``'points'``, ``'edges'`` or ``'faces'``.

        Returns
        -------
        List[int]
            List of neighbor cells IDs for the ind-th cell.

        Warnings
        --------
        For a :class:`pyvista.ExplicitStructuredGrid`, use :func:`pyvista.ExplicitStructuredGrid.neighbors`.

        See Also
        --------
        pyvista.DataSet.cell_neighbors_levels

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_airplane()

        Get the neighbor cell ids that have at least one point in common with
        the 0-th cell.

        >>> mesh.cell_neighbors(0, "points")
        [1, 2, 3, 388, 389, 11, 12, 395, 14, 209, 211, 212]

        Get the neighbor cell ids that have at least one edge in common with
        the 0-th cell.

        >>> mesh.cell_neighbors(0, "edges")
        [1, 3, 12]

        For unstructured grids with cells of dimension 3 (Tetrahedron for example),
        cell neighbors can be defined using faces.

        >>> mesh = examples.download_tetrahedron()
        >>> mesh.cell_neighbors(0, "faces")
        [1, 5, 7]

        Show a visual example.

        >>> from functools import partial
        >>> import pyvista
        >>> mesh = pyvista.Sphere(theta_resolution=10)
        >>>
        >>> pl = pyvista.Plotter(shape=(1, 2))
        >>> pl.link_views()
        >>> add_point_labels = partial(
        ...     pl.add_point_labels,
        ...     text_color="white",
        ...     font_size=20,
        ...     shape=None,
        ...     show_points=False,
        ... )
        >>>
        >>> for i, connection in enumerate(["points", "edges"]):
        ...     pl.subplot(0, i)
        ...     pl.view_yx()
        ...     _ = pl.add_title(
        ...         f"{connection.capitalize()} neighbors",
        ...         color="red",
        ...         shadow=True,
        ...         font_size=8,
        ...     )
        ...
        ...     # Add current cell
        ...     i_cell = 0
        ...     current_cell = mesh.extract_cells(i_cell)
        ...     _ = pl.add_mesh(
        ...         current_cell, show_edges=True, color="blue"
        ...     )
        ...     _ = add_point_labels(
        ...         current_cell.cell_centers().points,
        ...         labels=[f"{i_cell}"],
        ...     )
        ...
        ...     # Add neighbors
        ...     ids = mesh.cell_neighbors(i_cell, connection)
        ...     cells = mesh.extract_cells(ids)
        ...     _ = pl.add_mesh(cells, color="red", show_edges=True)
        ...     _ = add_point_labels(
        ...         cells.cell_centers().points,
        ...         labels=[f"{i}" for i in ids],
        ...     )
        ...
        ...     # Add other cells
        ...     ids.append(i_cell)
        ...     others = mesh.extract_cells(ids, invert=True)
        ...     _ = pl.add_mesh(others, show_edges=True)
        ...
        >>> pl.show()
        """
        if isinstance(self, _vtk.vtkExplicitStructuredGrid):
            raise TypeError("For an ExplicitStructuredGrid, use the `neighbors` method")

        # Build links as recommended:
        # https://vtk.org/doc/nightly/html/classvtkPolyData.html#adf9caaa01f72972d9a986ba997af0ac7
        if hasattr(self, "BuildLinks"):
            self.BuildLinks()

        needed = ["points", "edges", "faces"]
        if connections not in needed:
            raise ValueError(f'`connections` must be one of: {needed} (got "{connections}")')

        cell = self.get_cell(ind)

        iterators = {
            "points": cell.point_ids,
            "edges": range(cell.n_edges),
            "faces": range(cell.n_faces),
        }

        def generate_ids(i: int, connections: str):
            if connections == "points":
                ids = _vtk.vtkIdList()
                ids.InsertNextId(i)
                return ids
            elif connections == "edges":
                return cell.get_edge(i).GetPointIds()
            elif connections == "faces":
                return cell.get_face(i).GetPointIds()

        neighbors = set()
        for i in iterators[connections]:
            point_ids = generate_ids(i, connections)
            cell_ids = _vtk.vtkIdList()
            self.GetCellNeighbors(ind, point_ids, cell_ids)

            neighbors.update([cell_ids.GetId(i) for i in range(cell_ids.GetNumberOfIds())])

        return list(neighbors)

    def point_neighbors(self, ind: int) -> List[int]:
        """Get the point neighbors of the ind-th point.

        Parameters
        ----------
        ind : int
            Point ID.

        Returns
        -------
        List[int]
            List of neighbor points IDs for the ind-th point.

        See Also
        --------
        pyvista.DataSet.point_neighbors_levels

        Examples
        --------
        Get the point neighbors of the 0-th point.

        >>> import pyvista as pv
        >>> mesh = pv.Sphere(theta_resolution=10)
        >>> mesh.point_neighbors(0)
        [2, 226, 198, 170, 142, 114, 86, 254, 58, 30]

        Plot them.

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(mesh, show_edges=True)
        >>>
        >>> # Label the 0-th point
        >>> _ = pl.add_point_labels(
        ...     mesh.points[0], ["0"], text_color="blue", font_size=40
        ... )
        >>>
        >>> # Get the point neighbors and plot them
        >>> neighbors = mesh.point_neighbors(0)
        >>> _ = pl.add_point_labels(
        ...     mesh.points[neighbors],
        ...     labels=[f"{i}" for i in neighbors],
        ...     text_color="red",
        ...     font_size=40,
        ... )
        >>> pl.camera_position = "yx"
        >>> pl.camera.zoom(7.0)
        >>> pl.show()

        """
        if ind + 1 > self.n_points:
            raise IndexError(f'Invalid index {ind} for a dataset with {self.n_points} points.')

        out = []
        for cell in self.point_cell_ids(ind):
            out.extend([i for i in self.get_cell(cell).point_ids if i != ind])
        return list(set(out))

    def point_neighbors_levels(
        self, ind: int, n_levels: int = 1
    ) -> Generator[List[int], None, None]:
        """Get consecutive levels of point neighbors.

        Parameters
        ----------
        ind : int
            Point ID.

        n_levels : int, default: 1
            Number of levels to search for point neighbors.
            When equal to 1, it is equivalent to :func:`pyvista.DataSet.point_neighbors`.

        Returns
        -------
        generator[list[[int]]
            A generator of list of neighbor points IDs for the ind-th point.

        See Also
        --------
        pyvista.DataSet.point_neighbors

        Examples
        --------
        Get the point neighbors IDs starting from the 0-th point
        up until the third level.

        >>> import pyvista as pv
        >>> mesh = pv.Sphere(theta_resolution=10)
        >>> pt_nbr_levels = mesh.point_neighbors_levels(0, 3)
        >>> pt_nbr_levels = list(pt_nbr_levels)
        >>> pt_nbr_levels[0]
        [2, 226, 198, 170, 142, 114, 86, 30, 58, 254]
        >>> pt_nbr_levels[1]
        [3, 227, 255, 199, 171, 143, 115, 87, 59, 31]
        >>> pt_nbr_levels[2]
        [256, 32, 4, 228, 200, 172, 144, 116, 88, 60]

        Visualize these points IDs.

        >>> from functools import partial
        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(mesh, show_edges=True)
        >>>
        >>> # Define partial function to add point labels
        >>> add_point_labels = partial(
        ...     pl.add_point_labels,
        ...     text_color="white",
        ...     font_size=40,
        ...     point_size=10,
        ... )
        >>>
        >>> # Add the first point label
        >>> _ = add_point_labels(
        ...     mesh.points[0], labels=["0"], text_color="blue"
        ... )
        >>>
        >>> # Add the neighbors to the plot
        >>> neighbors = mesh.point_neighbors_levels(0, n_levels=3)
        >>> for i, ids in enumerate(neighbors, start=1):
        ...     _ = add_point_labels(
        ...         mesh.points[ids],
        ...         labels=[f"{i}"] * len(ids),
        ...         text_color="red",
        ...     )
        ...
        >>>
        >>> pl.view_yx()
        >>> pl.camera.zoom(4.0)
        >>> pl.show()
        """
        method = self.point_neighbors
        return self._get_levels_neihgbors(ind, n_levels, method)

    def cell_neighbors_levels(
        self, ind: int, connections: str = "points", n_levels: int = 1
    ) -> Generator[List[int], None, None]:
        """Get consecutive levels of cell neighbors.

        Parameters
        ----------
        ind : int
            Cell ID.

        connections : str, default: "points"
            Describe how the neighbor cell(s) must be connected to the current
            cell to be considered as a neighbor.
            Can be either ``'points'``, ``'edges'`` or ``'faces'``.

        n_levels : int, default: 1
            Number of levels to search for cell neighbors.
            When equal to 1, it is equivalent to :func:`pyvista.DataSet.point_neighbors`.

        Returns
        -------
        generator[list[int]]
            A generator of list of cell IDs for each level.

        Warnings
        --------
        For a :class:`pyvista.ExplicitStructuredGrid`, use :func:`pyvista.ExplicitStructuredGrid.neighbors`.

        See Also
        --------
        pyvista.DataSet.cell_neighbors

        Examples
        --------
        Get the cell neighbors IDs starting from the 0-th cell
        up until the third level.

        >>> import pyvista as pv
        >>> mesh = pv.Sphere(theta_resolution=10)
        >>> nbr_levels = mesh.cell_neighbors_levels(
        ...     0, connections="edges", n_levels=3
        ... )
        >>> nbr_levels = list(nbr_levels)
        >>> nbr_levels[0]
        [1, 21, 9]
        >>> nbr_levels[1]
        [2, 8, 74, 75, 20, 507]
        >>> nbr_levels[2]
        [128, 129, 3, 453, 7, 77, 23, 506]

        Visualize these cells IDs.

        >>> from functools import partial
        >>> pv.global_theme.color_cycler = [
        ...     'red',
        ...     'green',
        ...     'blue',
        ...     'purple',
        ... ]
        >>> pl = pv.Plotter()
        >>>
        >>> # Define partial function to add point labels
        >>> add_point_labels = partial(
        ...     pl.add_point_labels,
        ...     text_color="white",
        ...     font_size=40,
        ...     shape=None,
        ...     show_points=False,
        ... )
        >>>
        >>> # Add the 0-th cell to the plotter
        >>> cell = mesh.extract_cells(0)
        >>> _ = pl.add_mesh(cell, show_edges=True)
        >>> _ = add_point_labels(cell.cell_centers().points, labels=["0"])
        >>> other_ids = [0]
        >>>
        >>> # Add the neighbors to the plot
        >>> neighbors = mesh.cell_neighbors_levels(
        ...     0, connections="edges", n_levels=3
        ... )
        >>> for i, ids in enumerate(neighbors, start=1):
        ...     cells = mesh.extract_cells(ids)
        ...     _ = pl.add_mesh(cells, show_edges=True)
        ...     _ = add_point_labels(
        ...         cells.cell_centers().points, labels=[f"{i}"] * len(ids)
        ...     )
        ...     other_ids.extend(ids)
        ...
        >>>
        >>> # Add the cell IDs that are not neighbors (ie. the rest of the sphere)
        >>> cells = mesh.extract_cells(other_ids, invert=True)
        >>> _ = pl.add_mesh(cells, color="white", show_edges=True)
        >>>
        >>> pl.view_yx()
        >>> pl.camera.zoom(6.0)
        >>> pl.show()
        """
        method = partial(self.cell_neighbors, connections=connections)
        return self._get_levels_neihgbors(ind, n_levels, method)

    def _get_levels_neihgbors(
        self, ind: int, n_levels: int, method: Callable
    ) -> Generator[List[int], None, None]:
        """Provide helper method that yields neighbors ids."""
        neighbors = set(method(ind))
        yield list(neighbors)

        # Keep track of visited points or cells
        all_visited = neighbors.copy()
        all_visited.add(ind)

        for _ in range(n_levels - 1):
            # Get the neighbors for the next level.
            new_visited = set()
            for n in neighbors:
                new_neighbors = method(n)
                new_visited.update(new_neighbors)
            neighbors = new_visited

            # Only return the ones that have not been visited yet
            yield list(neighbors.difference(all_visited))
            all_visited.update(neighbors)

    def point_cell_ids(self, ind: int) -> List[int]:
        """Get the cell IDs that use the ind-th point.

        Implements vtkDataSet's `GetPointCells <https://vtk.org/doc/nightly/html/classvtkDataSet.html#a36d1d8f67ad67adf4d1a9cfb30dade49>`_.

        Parameters
        ----------
        ind : int
            Point ID.

        Returns
        -------
        List[int]
            List of cell IDs using the ind-th point.

        Examples
        --------
        Get the cell ids using the 0-th point.

        >>> import pyvista as pv
        >>> mesh = pv.Sphere(theta_resolution=10)
        >>> mesh.point_cell_ids(0)
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

        Plot them.

        >>> pl = pv.Plotter()
        >>> _ = pl.add_mesh(mesh, show_edges=True)
        >>>
        >>> # Label the 0-th point
        >>> _ = pl.add_point_labels(
        ...     mesh.points[0], ["0"], text_color="blue", font_size=20
        ... )
        >>>
        >>> # Get the cells ids using the 0-th point
        >>> ids = mesh.point_cell_ids(0)
        >>> cells = mesh.extract_cells(ids)
        >>> _ = pl.add_mesh(cells, color="red", show_edges=True)
        >>> centers = cells.cell_centers().points
        >>> _ = pl.add_point_labels(
        ...     centers,
        ...     labels=[f"{i}" for i in ids],
        ...     text_color="white",
        ...     font_size=20,
        ...     shape=None,
        ...     show_points=False,
        ... )
        >>>
        >>> # Plot the other cells
        >>> others = mesh.extract_cells(
        ...     [i for i in range(mesh.n_cells) if i not in ids]
        ... )
        >>> _ = pl.add_mesh(others, show_edges=True)
        >>>
        >>> pl.camera_position = "yx"
        >>> pl.camera.zoom(7.0)
        >>> pl.show()
        """
        # Build links as recommended:
        # https://vtk.org/doc/nightly/html/classvtkPolyData.html#adf9caaa01f72972d9a986ba997af0ac7
        if hasattr(self, "BuildLinks"):
            self.BuildLinks()

        ids = _vtk.vtkIdList()
        self.GetPointCells(ind, ids)
        return [ids.GetId(i) for i in range(ids.GetNumberOfIds())]

    def point_is_inside_cell(
        self, ind: int, point: Union[VectorArray, NumericArray]
    ) -> Union[int, np.ndarray]:
        """Return whether one or more points are inside a cell.

        .. versionadded:: 0.35.0

        Parameters
        ----------
        ind : int
            Cell ID.

        point : array_like[float]
            Coordinates of point to query (length 3) or a
            :class:`numpy.ndarray` of ``n`` points with shape ``(n, 3)``.

        Returns
        -------
        bool or numpy.ndarray
            Whether point(s) is/are inside cell. A scalar bool is only returned if
            the input point has shape ``(3,)``.

        Examples
        --------
        >>> from pyvista import examples
        >>> mesh = examples.load_hexbeam()
        >>> mesh.get_cell(0).bounds
        (0.0, 0.5, 0.0, 0.5, 0.0, 0.5)
        >>> mesh.point_is_inside_cell(0, [0.2, 0.2, 0.2])
        True

        """
        if not isinstance(ind, (int, np.integer)):
            raise TypeError(f"ind must be an int, got {type(ind)}")

        if not 0 <= ind < self.n_cells:
            raise ValueError(f"ind must be >= 0 and < {self.n_cells}, got {ind}")

        co_point, singular = _coerce_pointslike_arg(point, copy=False)

        cell = self.GetCell(ind)
        npoints = cell.GetPoints().GetNumberOfPoints()

        closest_point = [0.0, 0.0, 0.0]
        sub_id = _vtk.mutable(0)
        pcoords = [0.0, 0.0, 0.0]
        dist2 = _vtk.mutable(0.0)
        weights = [0.0] * npoints

        in_cell = np.empty(shape=co_point.shape[0], dtype=np.bool_)
        for i, node in enumerate(co_point):
            is_inside = cell.EvaluatePosition(node, closest_point, sub_id, pcoords, dist2, weights)
            if not 0 <= is_inside <= 1:
                raise RuntimeError(
                    f"Computational difficulty encountered for point {node} in cell {ind}"
                )
            in_cell[i] = bool(is_inside)

        if singular:
            return in_cell[0]
        return in_cell


# pyvista/core/datasetattributes.py

"""Implements DataSetAttributes, which represents and manipulates datasets."""

from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union

import numpy as np

from pyvista import _vtk
import pyvista.utilities.helpers as helpers
from pyvista.utilities.helpers import FieldAssociation
from pyvista.utilities.misc import copy_vtk_array

from .._typing import Number
from .pyvista_ndarray import pyvista_ndarray

# from https://vtk.org/doc/nightly/html/vtkDataSetAttributes_8h_source.html
attr_type = [
    'SCALARS',  # 0
    'VECTORS',  # 1
    'NORMALS',  # 2
    'TCOORDS',  # 3
    'TENSORS',  # 4
    'GLOBALIDS',  # 5
    'PEDIGREEIDS',  # 6
    'EDGEFLAG',  # 7
    'TANGENTS',  # 8
    'RATIONALWEIGHTS',  # 9
    'HIGHERORDERDEGREES',  # 10
    '',  # 11  (not an attribute)
]

# used to check if default args have changed in pop
_SENTINEL = pyvista_ndarray([])


class DataSetAttributes(_vtk.VTKObjectWrapper):
    """Python friendly wrapper of ``vtk.DataSetAttributes``.

    This class provides the ability to pick one of the present arrays as the
    currently active array for each attribute type by implementing a
    ``dict`` like interface.

    When adding data arrays but not desiring to set them as active
    scalars or vectors, use :func:`DataSetAttributes.set_array`.

    When adding directional data (such as velocity vectors), use
    :func:`DataSetAttributes.set_vectors`.

    When adding non-directional data (such as temperature values or
    multi-component scalars like RGBA values), use
    :func:`DataSetAttributes.set_scalars`.

    .. versionchanged:: 0.32.0
        The ``[]`` operator no longer allows integers.  Use
        :func:`DataSetAttributes.get_array` to retrieve an array
        using an index.

    Parameters
    ----------
    vtkobject : vtkFieldData
        The vtk object to wrap as a DataSetAttribute, usually an
        instance of ``vtk.vtkCellData``, ``vtk.vtkPointData``, or
        ``vtk.vtkFieldData``.

    dataset : vtkDataSet
        The vtkDataSet containing the vtkobject.

    association : FieldAssociation
        The array association type of the vtkobject.

    Notes
    -----
    When printing out the point arrays, you can see which arrays are
    the active scalars, vectors, normals, and texture coordinates.
    In the arrays list, ``SCALARS`` denotes that these are the active
    scalars, ``VECTORS`` denotes that these arrays are tagged as the
    active vectors data (i.e. data with magnitude and direction) and
    so on.

    Examples
    --------
    Store data with point association in a DataSet.

    >>> import pyvista
    >>> mesh = pyvista.Cube()
    >>> mesh.point_data['my_data'] = range(mesh.n_points)
    >>> data = mesh.point_data['my_data']
    >>> data
    pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

    Change the data array and show that this is reflected in the DataSet.

    >>> data[:] = 0
    >>> mesh.point_data['my_data']
    pyvista_ndarray([0, 0, 0, 0, 0, 0, 0, 0])

    Remove the array.

    >>> del mesh.point_data['my_data']
    >>> 'my_data' in mesh.point_data
    False

    Print the available arrays from dataset attributes.

    >>> import numpy as np
    >>> mesh = pyvista.Plane(i_resolution=1, j_resolution=1)
    >>> mesh.point_data.set_array(range(4), 'my-data')
    >>> mesh.point_data.set_array(range(5, 9), 'my-other-data')
    >>> vectors0 = np.random.random((4, 3))
    >>> mesh.point_data.set_vectors(vectors0, 'vectors0')
    >>> vectors1 = np.random.random((4, 3))
    >>> mesh.point_data.set_vectors(vectors1, 'vectors1')
    >>> mesh.point_data
    pyvista DataSetAttributes
    Association     : POINT
    Active Scalars  : None
    Active Vectors  : vectors1
    Active Texture  : TextureCoordinates
    Active Normals  : Normals
    Contains arrays :
        Normals                 float32    (4, 3)               NORMALS
        TextureCoordinates      float32    (4, 2)               TCOORDS
        my-data                 int64      (4,)
        my-other-data           int64      (4,)
        vectors1                float64    (4, 3)               VECTORS
        vectors0                float64    (4, 3)

    """

    def __init__(
        self, vtkobject: _vtk.vtkFieldData, dataset: _vtk.vtkDataSet, association: FieldAssociation
    ):
        """Initialize DataSetAttributes."""
        super().__init__(vtkobject=vtkobject)
        self.dataset = dataset
        self.association = association

    def __repr__(self) -> str:
        """Printable representation of DataSetAttributes."""
        info = ['pyvista DataSetAttributes']
        array_info = ' None'
        if self:
            lines = []
            for i, (name, array) in enumerate(self.items()):
                if len(name) > 23:
                    name = f'{name[:20]}...'
                try:
                    arr_type = attr_type[self.IsArrayAnAttribute(i)]
                except (IndexError, TypeError, AttributeError):  # pragma: no cover
                    arr_type = ''

                # special treatment for vector data
                if self.association in [FieldAssociation.POINT, FieldAssociation.CELL]:
                    if name == self.active_vectors_name:
                        arr_type = 'VECTORS'

                line = f'{name[:23]:<24}{str(array.dtype):<11}{str(array.shape):<20} {arr_type}'.strip()
                lines.append(line)
            array_info = '\n    ' + '\n    '.join(lines)

        info.append(f'Association     : {self.association.name}')
        if self.association in [FieldAssociation.POINT, FieldAssociation.CELL]:
            info.append(f'Active Scalars  : {self.active_scalars_name}')
            info.append(f'Active Vectors  : {self.active_vectors_name}')
            info.append(f'Active Texture  : {self.active_t_coords_name}')
            info.append(f'Active Normals  : {self.active_normals_name}')

        info.append(f'Contains arrays :{array_info}')
        return '\n'.join(info)

    def get(self, key: str, value: Optional[Any] = None) -> Optional[pyvista_ndarray]:
        """Return the value of the item with the specified key.

        Parameters
        ----------
        key : str
            Name of the array item you want to return the value from.

        value : Any, optional
            A value to return if the key does not exist.  Default
            is ``None``.

        Returns
        -------
        Any
            Array if the ``key`` exists in the dataset, otherwise
            ``value``.

        Examples
        --------
        Show that the default return value for a non-existent key is
        ``None``.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)
        >>> mesh.point_data.get('my-other-data')

        """
        if key in self:
            return self[key]
        return value

    def __bool__(self) -> bool:
        """Return ``True`` when there are arrays present."""
        return bool(self.GetNumberOfArrays())

    def __getitem__(self, key: str) -> pyvista_ndarray:
        """Implement ``[]`` operator.

        Accepts an array name.
        """
        if not isinstance(key, str):
            raise TypeError('Only strings are valid keys for DataSetAttributes.')
        return self.get_array(key)

    def __setitem__(self, key: str, value: Union[np.ndarray, Sequence]):
        """Implement setting with the ``[]`` operator."""
        if not isinstance(key, str):
            raise TypeError('Only strings are valid keys for DataSetAttributes.')

        has_arr = key in self
        self.set_array(value, name=key)

        # do not make array active if it already exists.  This covers
        # an inplace update like self.point_data[key] += 1
        if has_arr:
            return

        # make active if not field data and there isn't already an active scalar
        if (
            self.association
            in [
                FieldAssociation.POINT,
                FieldAssociation.CELL,
            ]
            and self.active_scalars_name is None
        ):
            self.active_scalars_name = key

    def __delitem__(self, key: str):
        """Implement del with array name or index."""
        if not isinstance(key, str):
            raise TypeError('Only strings are valid keys for DataSetAttributes.')

        self.remove(key)

    def __contains__(self, name: str) -> bool:
        """Implement the ``in`` operator."""
        return name in self.keys()

    def __iter__(self) -> Iterator[str]:
        """Implement for loop iteration."""
        yield from self.keys()

    def __len__(self) -> int:
        """Return the number of arrays."""
        return self.VTKObject.GetNumberOfArrays()

    @property
    def active_scalars(self) -> Optional[pyvista_ndarray]:
        """Return the active scalars.

        .. versionchanged:: 0.32.0
            Can no longer be used to set the active scalars.  Either use
            :func:`DataSetAttributes.set_scalars` or if the array
            already exists, assign to
            :attr:`pyvista.DataSetAttributes.active_scalars_name`.

        Examples
        --------
        Associate point data to a simple cube mesh and show that the
        active scalars in the point array are the most recently added
        array.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data['data0'] = np.arange(mesh.n_points)
        >>> mesh.point_data.active_scalars
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        """
        self._raise_field_data_no_scalars_vectors()
        if self.GetScalars() is not None:
            array = pyvista_ndarray(
                self.GetScalars(), dataset=self.dataset, association=self.association
            )
            return self._patch_type(array)
        return None

    @property
    def active_vectors(self) -> Optional[np.ndarray]:
        """Return the active vectors as a pyvista_ndarray.

        .. versionchanged:: 0.32.0
            Can no longer be used to set the active vectors.  Either use
            :func:`DataSetAttributes.set_vectors` or if the array
            already exists, assign to
            :attr:`pyvista.DataSetAttributes.active_vectors_name`.

        Examples
        --------
        Associate point data to a simple cube mesh and show that the
        active vectors in the point array are the most recently added
        array.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Cube()
        >>> vectors = np.random.random((mesh.n_points, 3))
        >>> mesh.point_data.set_vectors(vectors, 'my-vectors')
        >>> vectors_out = mesh.point_data.active_vectors
        >>> vectors_out.shape
        (8, 3)

        """
        self._raise_field_data_no_scalars_vectors()
        vectors = self.GetVectors()
        if vectors is not None:
            return pyvista_ndarray(vectors, dataset=self.dataset, association=self.association)
        return None

    @property
    def valid_array_len(self) -> Optional[int]:
        """Return the length data should be when added to the dataset.

        If there are no restrictions, returns ``None``.

        Examples
        --------
        Show that valid array lengths match the number of points and
        cells for point and cell arrays, and there is no length limit
        for field data.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.n_points, mesh.n_cells
        (8, 6)
        >>> mesh.point_data.valid_array_len
        8
        >>> mesh.cell_data.valid_array_len
        6
        >>> mesh.field_data.valid_array_len is None
        True

        """
        if self.association == FieldAssociation.POINT:
            return self.dataset.GetNumberOfPoints()
        if self.association == FieldAssociation.CELL:
            return self.dataset.GetNumberOfCells()
        return None

    @property
    def active_t_coords(self) -> Optional[pyvista_ndarray]:
        """Return or set the active texture coordinates array.

        Returns
        -------
        pyvista.pyvista_ndarray
            Array of the active texture coordinates.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data.active_t_coords
        pyvista_ndarray([[ 0.,  0.],
                         [ 1.,  0.],
                         [ 1.,  1.],
                         [ 0.,  1.],
                         [-0.,  0.],
                         [-0.,  1.],
                         [-1.,  1.],
                         [-1.,  0.]], dtype=float32)

        """
        self._raise_no_t_coords()
        t_coords = self.GetTCoords()
        if t_coords is not None:
            return pyvista_ndarray(t_coords, dataset=self.dataset, association=self.association)
        return None

    @active_t_coords.setter
    def active_t_coords(self, t_coords: np.ndarray):
        self._raise_no_t_coords()
        if not isinstance(t_coords, np.ndarray):
            raise TypeError('Texture coordinates must be a numpy array')
        if t_coords.ndim != 2:
            raise ValueError('Texture coordinates must be a 2-dimensional array')
        valid_length = self.valid_array_len
        if t_coords.shape[0] != valid_length:
            raise ValueError(
                f'Number of texture coordinates ({t_coords.shape[0]}) must match number of points ({valid_length})'
            )
        if t_coords.shape[1] != 2:
            raise ValueError(
                f'Texture coordinates must only have 2 components, not ({t_coords.shape[1]})'
            )
        vtkarr = _vtk.numpyTovtkDataArray(t_coords, name='Texture Coordinates')
        self.SetTCoords(vtkarr)
        self.Modified()

    @property
    def active_t_coords_name(self) -> Optional[str]:
        """Name of the active texture coordinates array.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data.active_t_coords_name
        'TCoords'

        """
        self._raise_no_t_coords()
        if self.GetTCoords() is not None:
            return str(self.GetTCoords().GetName())
        return None

    @active_t_coords_name.setter
    def active_t_coords_name(self, name: str) -> None:
        if name is None:
            self.SetActiveTCoords(None)
            return

        self._raise_no_t_coords()
        dtype = self[name].dtype
        # only vtkDataArray subclasses can be set as active attributes
        if np.issubdtype(dtype, np.number) or dtype == bool:
            self.SetActiveTCoords(name)

    def get_array(self, key: Union[str, int]) -> pyvista_ndarray:
        """Get an array in this object.

        Parameters
        ----------
        key : str | int
            The name or index of the array to return.  Arrays are
            ordered within VTK DataSetAttributes, and this feature is
            mirrored here.

        Returns
        -------
        pyvista.pyvista_ndarray
            Returns a :class:`pyvista.pyvista_ndarray`.

        Raises
        ------
        KeyError
            If the key does not exist.

        Notes
        -----
        This is provided since arrays are ordered within VTK and can
        be indexed via an int.  When getting an array, you can just
        use the key of the array with the ``[]`` operator with the
        name of the array.

        Examples
        --------
        Store data with point association in a DataSet.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)

        Access using an index.

        >>> mesh.point_data.get_array(0)
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        Access using a key.

        >>> mesh.point_data.get_array('my_data')
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        """
        self._raise_index_out_of_bounds(index=key)
        vtk_arr = self.GetArray(key)
        if vtk_arr is None:
            vtk_arr = self.GetAbstractArray(key)
            if vtk_arr is None:
                raise KeyError(f'{key}')
        narray = pyvista_ndarray(vtk_arr, dataset=self.dataset, association=self.association)
        return self._patch_type(narray)

    def _patch_type(self, narray):
        """Check if array needs to be represented as a different type."""
        name = narray.VTKObject.GetName()
        if name in self.dataset._association_bitarray_names[self.association.name]:
            narray = narray.view(np.bool_)  # type: ignore
        elif name in self.dataset._association_complex_names[self.association.name]:
            if narray.dtype == np.float32:
                narray = narray.view(np.complex64)  # type: ignore
            if narray.dtype == np.float64:
                narray = narray.view(np.complex128)  # type: ignore
            # remove singleton dimensions to match the behavior of the rest of 1D
            # VTK arrays
            narray = narray.squeeze()
        return narray

    def set_array(
        self, data: Union[Sequence[Number], Number, np.ndarray], name: str, deep_copy=False
    ) -> None:
        """Add an array to this object.

        Use this method when adding arrays to the DataSet.  If
        needed, these arrays can later be assigned to become the
        active scalars, vectors, normals, or texture coordinates with:

        * :attr:`active_scalars_name <DataSetAttributes.active_scalars_name>`
        * :attr:`active_vectors_name <DataSetAttributes.active_vectors_name>`
        * :attr:`active_normals_name <DataSetAttributes.active_normals_name>`
        * :attr:`active_t_coords_name <DataSetAttributes.active_t_coords_name>`

        Parameters
        ----------
        data : float | array_like[float]
            A :class:`pyvista.pyvista_ndarray`, :class:`numpy.ndarray`,
            ``list``, ``tuple`` or scalar value.

        name : str
            Name to assign to the data.  If this name already exists,
            it will be overwritten.

        deep_copy : bool, optional
            When ``True`` makes a full copy of the array.

        Notes
        -----
        You can simply use the ``[]`` operator to add an array to the
        dataset.  Note that this will automatically become the active
        scalars.

        Examples
        --------
        Add a point array to a mesh.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> data = range(mesh.n_points)
        >>> mesh.point_data.set_array(data, 'my-data')
        >>> mesh.point_data['my-data']
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        Add a cell array to a mesh.

        >>> cell_data = range(mesh.n_cells)
        >>> mesh.cell_data.set_array(cell_data, 'my-data')
        >>> mesh.cell_data['my-data']
        pyvista_ndarray([0, 1, 2, 3, 4, 5])

        Add field data to a mesh.

        >>> field_data = range(3)
        >>> mesh.field_data.set_array(field_data, 'my-data')
        >>> mesh.field_data['my-data']
        pyvista_ndarray([0, 1, 2])

        """
        if not isinstance(name, str):
            raise TypeError('`name` must be a string')

        vtk_arr = self._prepare_array(data, name, deep_copy)
        self.VTKObject.AddArray(vtk_arr)
        self.VTKObject.Modified()

    def set_scalars(
        self, scalars: Union[Sequence[Number], Number, np.ndarray], name='scalars', deep_copy=False
    ):
        """Set the active scalars of the dataset with an array.

        In VTK and PyVista, scalars are a quantity that has no
        direction.  This can include data with multiple components
        (such as RGBA values) or just one component (such as
        temperature data).

        See :func:`DataSetAttributes.set_vectors` when adding arrays
        that contain magnitude and direction.

        Parameters
        ----------
        scalars : float | array_like[float]
            A :class:`pyvista.pyvista_ndarray`, :class:`numpy.ndarray`, ``list``,
            ``tuple`` or scalar value.

        name : str, default: 'scalars'
            Name to assign the scalars.

        deep_copy : bool, default: False
            When ``True`` makes a full copy of the array.

        Notes
        -----
        When adding directional data (such as velocity vectors), use
        :func:`DataSetAttributes.set_vectors`.

        Complex arrays will be represented internally as a 2 component float64
        array. This is due to limitations of VTK's native datatypes.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> scalars = range(mesh.n_points)
        >>> mesh.point_data.set_scalars(scalars, 'my-scalars')
        >>> mesh.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : my-scalars
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            my-scalars              int64      (8,)                 SCALARS

        """
        vtk_arr = self._prepare_array(scalars, name, deep_copy)
        self.VTKObject.SetScalars(vtk_arr)
        self.VTKObject.Modified()

    def set_vectors(
        self, vectors: Union[Sequence[Number], Number, np.ndarray], name: str, deep_copy=False
    ):
        """Set the active vectors of this data attribute.

        Vectors are a quantity that has magnitude and direction, such
        as normal vectors or a velocity field.

        The vectors data must contain three components per cell or
        point.  Use :func:`DataSetAttributes.set_scalars` when
        adding non-directional data.

        Parameters
        ----------
        vectors : float | array_like[float]
            A :class:`pyvista.pyvista_ndarray`, :class:`numpy.ndarray`,
            ``list``, or ``tuple``.  Must match the number of cells or points
            of the dataset.

        name : str
            Name of the vectors.

        deep_copy : bool, default: False
            When ``True`` makes a full copy of the array.  When
            ``False``, the data references the original array
            without copying it.

        Notes
        -----
        PyVista and VTK treats vectors and scalars differently when
        performing operations. Vector data, unlike scalar data, is
        rotated along with the geometry when the DataSet is passed
        through a transformation filter.

        When adding non-directional data (such temperature values or
        multi-component scalars like RGBA values), you can also use
        :func:`DataSetAttributes.set_scalars`.

        Examples
        --------
        Add random vectors to a mesh as point data.

        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> vectors = np.random.random((mesh.n_points, 3))
        >>> mesh.point_data.set_vectors(vectors, 'my-vectors')
        >>> mesh.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : None
        Active Vectors  : my-vectors
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            my-vectors              float64    (8, 3)               VECTORS

        """
        # prepare the array and add an attribute so that we can track this as a vector
        vtk_arr = self._prepare_array(vectors, name, deep_copy)

        n_comp = vtk_arr.GetNumberOfComponents()
        if n_comp != 3:
            raise ValueError(f'Vector array should contain 3 components, got {n_comp}')

        # check if there are current vectors, if so, we need to keep
        # this array around since setting active vectors will remove
        # this array.
        current_vectors = self.GetVectors()

        # now we can set the active vectors and add back in the old vectors as an array
        self.VTKObject.SetVectors(vtk_arr)
        if current_vectors is not None:
            self.VTKObject.AddArray(current_vectors)

        self.VTKObject.Modified()

    def _prepare_array(
        self, data: Union[Sequence[Number], Number, np.ndarray], name: str, deep_copy: bool
    ) -> _vtk.vtkDataSet:
        """Prepare an array to be added to this dataset.

        Notes
        -----
        This method also adds metadata necessary for VTK to support non-VTK
        compatible datatypes like ``numpy.complex128`` or ``numpy.bool_`` to
        the underlying dataset.

        """
        if data is None:
            raise TypeError('``data`` cannot be None.')

        # attempt to reuse the existing pointer to underlying VTK data
        if isinstance(data, pyvista_ndarray):
            # pyvista_ndarray already contains the reference to the vtk object
            # pyvista needs to use the copy of this object rather than wrapping
            # the array (which leaves a C++ pointer uncollected.
            if data.VTKObject is not None:
                # VTK doesn't support strides, therefore we can't directly
                # point to the underlying object
                if data.flags.c_contiguous:
                    # no reason to return a shallow copy if the array and name
                    # are identical, just return the underlying array name
                    if not deep_copy and isinstance(name, str) and data.VTKObject.GetName() == name:
                        return data.VTKObject

                    vtk_arr = copy_vtk_array(data.VTKObject, deep=deep_copy)
                    if isinstance(name, str):
                        vtk_arr.SetName(name)
                    return vtk_arr

        # convert to numpy type if necessary
        data = np.asanyarray(data)

        if self.association == FieldAssociation.POINT:
            array_len = self.dataset.GetNumberOfPoints()
        elif self.association == FieldAssociation.CELL:
            array_len = self.dataset.GetNumberOfCells()
        else:
            array_len = data.shape[0] if isinstance(data, np.ndarray) else 1

        # Fixup input array length for scalar input
        if not isinstance(data, np.ndarray) or np.ndim(data) == 0:
            tmparray = np.empty(array_len)
            tmparray.fill(data)
            data = tmparray
        if data.shape[0] != array_len:
            raise ValueError(f'data length of ({data.shape[0]}) != required length ({array_len})')

        # reset data association
        if name in self.dataset._association_bitarray_names[self.association.name]:
            self.dataset._association_bitarray_names[self.association.name].remove(name)
        if name in self.dataset._association_complex_names[self.association.name]:
            self.dataset._association_complex_names[self.association.name].remove(name)

        if data.dtype == np.bool_:
            self.dataset._association_bitarray_names[self.association.name].add(name)
            data = data.view(np.uint8)
        elif np.issubdtype(data.dtype, np.complexfloating):
            if data.dtype not in (np.complex64, np.complex128):
                raise ValueError(
                    'Only numpy.complex64 or numpy.complex128 is supported when '
                    'setting dataset attributes'
                )

            if data.ndim != 1:
                if data.shape[1] != 1:
                    raise ValueError('Complex data must be single dimensional.')
            self.dataset._association_complex_names[self.association.name].add(name)

            # complex data is stored internally as a contiguous 2 component
            # float arrays
            if data.dtype == np.complex64:
                data = data.view(np.float32).reshape(-1, 2)
            else:
                data = data.view(np.float64).reshape(-1, 2)

        shape = data.shape
        if data.ndim == 3:
            # Array of matrices. We need to make sure the order in
            # memory is right.  If row major (C/C++),
            # transpose. VTK wants column major (Fortran order). The deep
            # copy later will make sure that the array is contiguous.
            # If column order but not contiguous, transpose so that the
            # deep copy below does not happen.
            size = data.dtype.itemsize
            if (data.strides[1] / size == 3 and data.strides[2] / size == 1) or (
                data.strides[1] / size == 1
                and data.strides[2] / size == 3
                and not data.flags.contiguous
            ):
                data = data.transpose(0, 2, 1)

        # If array is not contiguous, make a deep copy that is contiguous
        if not data.flags.contiguous:
            data = np.ascontiguousarray(data)

        # Flatten array of matrices to array of vectors
        if len(shape) == 3:
            data = data.reshape(shape[0], shape[1] * shape[2])

        # Swap bytes from big to little endian.
        if data.dtype.byteorder == '>':
            data = data.byteswap(inplace=True)

        # this handles the case when an input array is directly added to the
        # output. We want to make sure that the array added to the output is not
        # referring to the input dataset.
        copy = pyvista_ndarray(data)

        return helpers.convert_array(copy, name, deep=deep_copy)

    def remove(self, key: str) -> None:
        """Remove an array.

        Parameters
        ----------
        key : str
            The name of the array to remove.

        Notes
        -----
        You can also use the ``del`` statement.

        Examples
        --------
        Add a point data array to a DataSet and then remove it.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)
        >>> mesh.point_data.remove('my_data')

        Show that the array no longer exists in ``point_data``.

        >>> 'my_data' in mesh.point_data
        False

        """
        if not isinstance(key, str):
            raise TypeError('Only strings are valid keys for DataSetAttributes.')

        if key not in self:
            raise KeyError(f'{key} not present.')

        try:
            self.dataset._association_bitarray_names[self.association.name].remove(key)
        except KeyError:
            pass
        self.VTKObject.RemoveArray(key)
        self.VTKObject.Modified()

    def pop(self, key: str, default=_SENTINEL) -> pyvista_ndarray:
        """Remove an array and return it.

        Parameters
        ----------
        key : str
            The name of the array to remove and return.

        default : Any, optional
            If default is not given and key is not in the dictionary,
            a KeyError is raised.

        Returns
        -------
        pyvista_ndarray
            Requested array.

        Examples
        --------
        Add a point data array to a DataSet and then remove it.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)
        >>> mesh.point_data.pop('my_data')
        pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7])

        Show that the array no longer exists in ``point_data``.

        >>> 'my_data' in mesh.point_data
        False

        """
        if not isinstance(key, str):
            raise TypeError('Only strings are valid keys for DataSetAttributes.')

        if key not in self:
            if default is _SENTINEL:
                raise KeyError(f'{key} not present.')
            return default

        narray = self.get_array(key)

        self.remove(key)
        return narray

    def items(self) -> List[Tuple[str, pyvista_ndarray]]:
        """Return a list of (array name, array value) tuples.

        Returns
        -------
        list
            List of keys and values.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.cell_data['data0'] = [0] * mesh.n_cells
        >>> mesh.cell_data['data1'] = range(mesh.n_cells)
        >>> mesh.cell_data.items()
        [('data0', pyvista_ndarray([0, 0, 0, 0, 0, 0])), ('data1', pyvista_ndarray([0, 1, 2, 3, 4, 5]))]

        """
        return list(zip(self.keys(), self.values()))

    def keys(self) -> List[str]:
        """Return the names of the arrays as a list.

        Returns
        -------
        list
            List of keys.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.clear_data()
        >>> mesh.point_data['data0'] = [0] * mesh.n_points
        >>> mesh.point_data['data1'] = range(mesh.n_points)
        >>> mesh.point_data.keys()
        ['data0', 'data1']

        """
        keys = []
        for i in range(self.GetNumberOfArrays()):
            array = self.VTKObject.GetAbstractArray(i)
            name = array.GetName()
            if name:
                keys.append(name)
            else:  # pragma: no cover
                # Assign this array a name
                name = f'Unnamed_{i}'
                array.SetName(name)
                keys.append(name)
        return keys

    def values(self) -> List[pyvista_ndarray]:
        """Return the arrays as a list.

        Returns
        -------
        list
            List of arrays.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.cell_data['data0'] = [0] * mesh.n_cells
        >>> mesh.cell_data['data1'] = range(mesh.n_cells)
        >>> mesh.cell_data.values()
        [pyvista_ndarray([0, 0, 0, 0, 0, 0]), pyvista_ndarray([0, 1, 2, 3, 4, 5])]

        """
        return [self.get_array(name) for name in self.keys()]

    def clear(self):
        """Remove all arrays in this object.

        Examples
        --------
        Add an array to ``point_data`` to a DataSet and then clear the
        point_data.

        >>> import pyvista
        >>> mesh = pyvista.Cube()
        >>> mesh.clear_data()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)
        >>> len(mesh.point_data)
        1
        >>> mesh.point_data.clear()
        >>> len(mesh.point_data)
        0

        """
        for array_name in self.keys():
            self.remove(key=array_name)

    def update(self, array_dict: Union[Dict[str, np.ndarray], 'DataSetAttributes']):
        """Update arrays in this object from another dictionary or dataset attributes.

        For each key, value given, add the pair. If it already exists, replace
        it with the new array. These arrays will be copied.

        Parameters
        ----------
        array_dict : dict, DataSetAttributes
            A dictionary of ``(array name, :class:`numpy.ndarray`)`` or a
            :class:`pyvista.DataSetAttributes`.

        Examples
        --------
        Add two arrays to ``point_data`` using ``update``.

        >>> import numpy as np
        >>> from pyvista import examples
        >>> mesh = examples.load_uniform()
        >>> n = len(mesh.point_data)
        >>> arrays = {
        ...     'foo': np.arange(mesh.n_points),
        ...     'rand': np.random.random(mesh.n_points),
        ... }
        >>> mesh.point_data.update(arrays)
        >>> mesh.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : Spatial Point Data
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : None
        Contains arrays :
            Spatial Point Data      float64    (1000,)              SCALARS
            foo                     int64      (1000,)
            rand                    float64    (1000,)

        """
        for name, array in array_dict.items():
            self[name] = array.copy()

    def _raise_index_out_of_bounds(self, index: Any):
        if isinstance(index, int):
            max_index = self.VTKObject.GetNumberOfArrays()
            if not 0 <= index < max_index:
                raise KeyError(f'Array index ({index}) out of range [0, {max_index - 1}]')

    def _raise_field_data_no_scalars_vectors(self):
        """Raise a TypeError if FieldData."""
        if self.association == FieldAssociation.NONE:
            raise TypeError('FieldData does not have active scalars or vectors.')

    @property
    def active_scalars_name(self) -> Optional[str]:
        """Name of the active scalars.

        Examples
        --------
        Add two arrays to the mesh point data. Note how the first array becomes
        the active scalars since the ``mesh`` contained no scalars.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.point_data['my_data'] = range(mesh.n_points)
        >>> mesh.point_data['my_other_data'] = range(mesh.n_points)
        >>> mesh.point_data.active_scalars_name
        'my_data'

        Set the name of the active scalars.

        >>> mesh.point_data.active_scalars_name = 'my_other_data'
        >>> mesh.point_data.active_scalars_name
        'my_other_data'

        """
        if self.GetScalars() is not None:
            name = self.GetScalars().GetName()
            if name is None:
                # Getting the keys has the side effect of naming "unnamed" arrays
                self.keys()
                name = self.GetScalars().GetName()
            return str(name)
        return None

    @active_scalars_name.setter
    def active_scalars_name(self, name: str) -> None:
        # permit setting no active scalars
        if name is None:
            self.SetActiveScalars(None)
            return
        self._raise_field_data_no_scalars_vectors()
        dtype = self[name].dtype
        # only vtkDataArray subclasses can be set as active attributes
        if np.issubdtype(dtype, np.number) or dtype == bool:
            self.SetActiveScalars(name)

    @property
    def active_vectors_name(self) -> Optional[str]:
        """Name of the active vectors.

        Examples
        --------
        >>> import pyvista
        >>> import numpy as np
        >>> mesh = pyvista.Sphere()
        >>> mesh.point_data.set_vectors(
        ...     np.random.random((mesh.n_points, 3)), 'my-vectors'
        ... )
        >>> mesh.point_data.active_vectors_name
        'my-vectors'

        """
        if self.GetVectors() is not None:
            return str(self.GetVectors().GetName())
        return None

    @active_vectors_name.setter
    def active_vectors_name(self, name: str) -> None:
        # permit setting no active
        if name is None:
            self.SetActiveVectors(None)
            return
        self._raise_field_data_no_scalars_vectors()
        if name not in self:
            raise KeyError(f'DataSetAttribute does not contain "{name}"')
        # verify that the array has the correct number of components
        n_comp = self.GetArray(name).GetNumberOfComponents()
        if n_comp != 3:
            raise ValueError(f'{name} needs 3 components, has ({n_comp})')
        self.SetActiveVectors(name)

    def __eq__(self, other: Any) -> bool:
        """Test dict-like equivalency."""
        # here we check if other is the same class or a subclass of self.
        if not isinstance(other, type(self)):
            return False

        if set(self.keys()) != set(other.keys()):
            return False

        # verify the value of the arrays
        for key, value in other.items():
            if not np.array_equal(value, self[key]):
                return False

        # check the name of the active attributes
        if self.association != FieldAssociation.NONE:
            for name in ['scalars', 'vectors', 't_coords', 'normals']:
                attr = f'active_{name}_name'
                if getattr(other, attr) != getattr(self, attr):
                    return False

        return True

    @property
    def active_normals(self) -> Optional[pyvista_ndarray]:
        """Return or set the normals.

        Returns
        -------
        pyvista_ndarray
            Normals of this dataset attribute.  ``None`` if no
            normals have been set.

        Notes
        -----
        Field data will have no normals.

        Examples
        --------
        First, compute cell normals.

        >>> import pyvista
        >>> mesh = pyvista.Plane(i_resolution=1, j_resolution=1)
        >>> mesh.point_data
        pyvista DataSetAttributes
        Association     : POINT
        Active Scalars  : None
        Active Vectors  : None
        Active Texture  : TextureCoordinates
        Active Normals  : Normals
        Contains arrays :
            Normals                 float32    (4, 3)               NORMALS
            TextureCoordinates      float32    (4, 2)               TCOORDS

        >>> mesh.point_data.active_normals
        pyvista_ndarray([[0.000000e+00,  0.000000e+00, -1.000000e+00],
                         [0.000000e+00,  0.000000e+00, -1.000000e+00],
                         [0.000000e+00,  0.000000e+00, -1.000000e+00],
                         [0.000000e+00,  0.000000e+00, -1.000000e+00]],
                        dtype=float32)

        Assign normals to the cell arrays.  An array will be added
        named ``"Normals"``.

        >>> mesh.cell_data.active_normals = [[0.0, 0.0, 1.0]]
        >>> mesh.cell_data
        pyvista DataSetAttributes
        Association     : CELL
        Active Scalars  : None
        Active Vectors  : None
        Active Texture  : None
        Active Normals  : Normals
        Contains arrays :
            Normals                 float64    (1, 3)               NORMALS

        """
        self._raise_no_normals()
        vtk_normals = self.GetNormals()
        if vtk_normals is not None:
            return pyvista_ndarray(vtk_normals, dataset=self.dataset, association=self.association)
        return None

    @active_normals.setter
    def active_normals(self, normals: Union[Sequence[Number], np.ndarray]):
        self._raise_no_normals()
        normals = np.asarray(normals)
        if normals.ndim != 2:
            raise ValueError('Normals must be a 2-dimensional array')
        valid_length = self.valid_array_len
        if normals.shape[0] != valid_length:
            raise ValueError(
                f'Number of normals ({normals.shape[0]}) must match number of points ({valid_length})'
            )
        if normals.shape[1] != 3:
            raise ValueError(f'Normals must have exactly 3 components, not ({normals.shape[1]})')

        vtkarr = _vtk.numpyTovtkDataArray(normals, name='Normals')
        self.SetNormals(vtkarr)
        self.Modified()

    @property
    def active_normals_name(self) -> Optional[str]:
        """Return or set the name of the normals array.

        Returns
        -------
        str
            Name of the active normals array.

        Examples
        --------
        First, compute cell normals.

        >>> import pyvista
        >>> mesh = pyvista.Plane(i_resolution=1, j_resolution=1)
        >>> mesh_w_normals = mesh.compute_normals()
        >>> mesh_w_normals.point_data.active_normals_name
        'Normals'

        """
        self._raise_no_normals()
        if self.GetNormals() is not None:
            return str(self.GetNormals().GetName())
        return None

    @active_normals_name.setter
    def active_normals_name(self, name: str) -> None:
        # permit setting no active
        if name is None:
            self.SetActiveNormals(None)
            return
        self._raise_no_normals()
        self.SetActiveNormals(name)

    def _raise_no_normals(self):
        """Raise AttributeError when attempting access normals for field data."""
        if self.association == FieldAssociation.NONE:
            raise AttributeError('FieldData does not have active normals.')

    def _raise_no_t_coords(self):
        """Raise AttributeError when attempting access t_coords for field data."""
        if self.association == FieldAssociation.NONE:
            raise AttributeError('FieldData does not have active texture coordinates.')


# pyvista/core/errors.py

"""PyVista specific errors."""

CAMERA_ERROR_MESSAGE = """Invalid camera description
Camera description must be one of the following:

Iterable containing position, focal_point, and view up.  For example:
[(2.0, 5.0, 13.0), (0.0, 0.0, 0.0), (-0.7, -0.5, 0.3)]

Iterable containing a view vector.  For example:
[-1.0, 2.0, -5.0]

A string containing the plane orthogonal to the view direction.  For example:
'xy'
"""


class NotAllTrianglesError(ValueError):
    """Exception when a mesh does not contain all triangles."""

    def __init__(self, message='Mesh must consist of only triangles'):
        """Empty init."""
        ValueError.__init__(self, message)


class InvalidCameraError(ValueError):
    """Exception when passed an invalid camera."""

    def __init__(self, message=CAMERA_ERROR_MESSAGE):
        """Empty init."""
        ValueError.__init__(self, message)


class DeprecationError(RuntimeError):
    """Used for depreciated methods and functions."""

    def __init__(self, message='This feature has been depreciated'):
        """Empty init."""
        RuntimeError.__init__(self, message)


class VTKVersionError(RuntimeError):
    """Requested feature is not supported by the installed VTK version."""

    def __init__(
        self, message='The requested feature is not supported by the installed VTK version.'
    ):
        """Empty init."""
        RuntimeError.__init__(self, message)


class PointSetNotSupported(TypeError):
    """Requested filter or property is not supported by the PointSet class."""

    def __init__(self, message='The requested operation is not supported for PointSets.'):
        """Empty init."""
        TypeError.__init__(self, message)


class PointSetCellOperationError(PointSetNotSupported):
    """Requested filter or property is not supported by the PointSet class."""

    def __init__(self, message='Cell operations are not supported. PointSets contain no cells.'):
        """Empty init."""
        PointSetNotSupported.__init__(self, message)


class PointSetDimensionReductionError(PointSetNotSupported):
    """Requested filter or property is not supported by the PointSet class."""

    def __init__(
        self, message='Slice and other dimension reducing filters are not supported on PointSets.'
    ):
        """Empty init."""
        PointSetNotSupported.__init__(self, message)


# pyvista/core/grid.py

"""Sub-classes for vtk.vtkRectilinearGrid and vtk.vtkImageData."""
from functools import wraps
import pathlib
from typing import Sequence, Tuple, Union
import warnings

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.core.dataset import DataSet
from pyvista.core.filters import RectilinearGridFilters, UniformGridFilters, _get_output
from pyvista.utilities import abstract_class, assert_empty_kwargs
import pyvista.utilities.helpers as helpers
from pyvista.utilities.misc import PyVistaDeprecationWarning, raise_has_duplicates


@abstract_class
class Grid(DataSet):
    """A class full of common methods for non-pointset grids."""

    def __init__(self, *args, **kwargs):
        """Initialize the grid."""
        super().__init__()

    @property
    def dimensions(self) -> Tuple[int, int, int]:
        """Return the grid's dimensions.

        These are effectively the number of points along each of the
        three dataset axes.

        Examples
        --------
        Create a uniform grid with dimensions ``(1, 2, 3)``.

        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(2, 3, 4))
        >>> grid.dimensions
        (2, 3, 4)
        >>> grid.plot(show_edges=True)

        Set the dimensions to ``(3, 4, 5)``

        >>> grid.dimensions = (3, 4, 5)
        >>> grid.plot(show_edges=True)

        """
        return self.GetDimensions()

    @dimensions.setter
    def dimensions(self, dims: Sequence[int]):
        """Set the dataset dimensions."""
        self.SetDimensions(*dims)
        self.Modified()

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = DataSet._get_attrs(self)
        attrs.append(("Dimensions", self.dimensions, "{:d}, {:d}, {:d}"))
        return attrs


class RectilinearGrid(_vtk.vtkRectilinearGrid, Grid, RectilinearGridFilters):
    """Dataset with variable spacing in the three coordinate directions.

    Can be initialized in several ways:

    * Create empty grid
    * Initialize from a ``vtk.vtkRectilinearGrid`` object
    * Initialize directly from the point arrays

    Parameters
    ----------
    uinput : str, pathlib.Path, vtk.vtkRectilinearGrid, numpy.ndarray, optional
        Filename, dataset, or array to initialize the rectilinear grid from. If a
        filename is passed, pyvista will attempt to load it as a
        :class:`RectilinearGrid`. If passed a ``vtk.vtkRectilinearGrid``, it
        will be wrapped. If a :class:`numpy.ndarray` is passed, this will be
        loaded as the x range.

    y : numpy.ndarray, optional
        Coordinates of the points in y direction. If this is passed, ``uinput``
        must be a :class:`numpy.ndarray`.

    z : numpy.ndarray, optional
        Coordinates of the points in z direction. If this is passed, ``uinput``
        and ``y`` must be a :class:`numpy.ndarray`.

    check_duplicates : bool, optional
        Check for duplications in any arrays that are passed. Defaults to
        ``False``. If ``True``, an error is raised if there are any duplicate
        values in any of the array-valued input arguments.

    deep : bool, optional
        Whether to deep copy a ``vtk.vtkRectilinearGrid`` object.
        Default is ``False``.  Keyword only.

    Examples
    --------
    >>> import pyvista
    >>> import vtk
    >>> import numpy as np

    Create an empty grid.

    >>> grid = pyvista.RectilinearGrid()

    Initialize from a vtk.vtkRectilinearGrid object

    >>> vtkgrid = vtk.vtkRectilinearGrid()
    >>> grid = pyvista.RectilinearGrid(vtkgrid)

    Create from NumPy arrays.

    >>> xrng = np.arange(-10, 10, 2)
    >>> yrng = np.arange(-10, 10, 5)
    >>> zrng = np.arange(-10, 10, 1)
    >>> grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
    >>> grid.plot(show_edges=True)

    """

    _WRITERS = {'.vtk': _vtk.vtkRectilinearGridWriter, '.vtr': _vtk.vtkXMLRectilinearGridWriter}

    def __init__(self, *args, check_duplicates=False, deep=False, **kwargs):
        """Initialize the rectilinear grid."""
        super().__init__()

        if len(args) == 1:
            if isinstance(args[0], _vtk.vtkRectilinearGrid):
                if deep:
                    self.deep_copy(args[0])
                else:
                    self.shallow_copy(args[0])
            elif isinstance(args[0], (str, pathlib.Path)):
                self._from_file(args[0], **kwargs)
            elif isinstance(args[0], (np.ndarray, Sequence)):
                self._from_arrays(np.asanyarray(args[0]), None, None, check_duplicates)
            else:
                raise TypeError(f'Type ({type(args[0])}) not understood by `RectilinearGrid`')

        elif len(args) == 3 or len(args) == 2:
            arg0_is_arr = isinstance(args[0], (np.ndarray, Sequence))
            arg1_is_arr = isinstance(args[1], (np.ndarray, Sequence))
            if len(args) == 3:
                arg2_is_arr = isinstance(args[2], (np.ndarray, Sequence))
            else:
                arg2_is_arr = False

            if all([arg0_is_arr, arg1_is_arr, arg2_is_arr]):
                self._from_arrays(
                    np.asanyarray(args[0]),
                    np.asanyarray(args[1]),
                    np.asanyarray(args[2]),
                    check_duplicates,
                )
            elif all([arg0_is_arr, arg1_is_arr]):
                self._from_arrays(
                    np.asanyarray(args[0]), np.asanyarray(args[1]), None, check_duplicates
                )
            else:
                raise TypeError("Arguments not understood by `RectilinearGrid`.")

    def __repr__(self):
        """Return the default representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the str representation."""
        return DataSet.__str__(self)

    def _update_dimensions(self):
        """Update the dimensions if coordinates have changed."""
        return self.SetDimensions(len(self.x), len(self.y), len(self.z))

    def _from_arrays(
        self, x: np.ndarray, y: np.ndarray, z: np.ndarray, check_duplicates: bool = False
    ):
        """Create VTK rectilinear grid directly from numpy arrays.

        Each array gives the uniques coordinates of the mesh along each axial
        direction. To help ensure you are using this correctly, we take the unique
        values of each argument.

        Parameters
        ----------
        x : numpy.ndarray
            Coordinates of the points in x direction.

        y : numpy.ndarray
            Coordinates of the points in y direction.

        z : numpy.ndarray
            Coordinates of the points in z direction.

        check_duplicates : bool, optional
            Check for duplications in any arrays that are passed.

        """
        # Set the coordinates along each axial direction
        # Must at least be an x array
        if check_duplicates:
            raise_has_duplicates(x)

        # edges are shown as triangles if x is not floating point
        if not np.issubdtype(x.dtype, np.floating):
            x = x.astype(float)
        self.SetXCoordinates(helpers.convert_array(x.ravel()))
        if y is not None:
            if check_duplicates:
                raise_has_duplicates(y)
            if not np.issubdtype(y.dtype, np.floating):
                y = y.astype(float)
            self.SetYCoordinates(helpers.convert_array(y.ravel()))
        if z is not None:
            if check_duplicates:
                raise_has_duplicates(z)
            if not np.issubdtype(z.dtype, np.floating):
                z = z.astype(float)
            self.SetZCoordinates(helpers.convert_array(z.ravel()))
        # Ensure dimensions are properly set
        self._update_dimensions()

    @property
    def meshgrid(self) -> list:
        """Return a meshgrid of numpy arrays for this mesh.

        This simply returns a :func:`numpy.meshgrid` of the
        coordinates for this mesh in ``ij`` indexing. These are a copy
        of the points of this mesh.

        """
        return np.meshgrid(self.x, self.y, self.z, indexing='ij')

    @property  # type: ignore
    def points(self) -> np.ndarray:  # type: ignore
        """Return a copy of the points as an n by 3 numpy array.

        Notes
        -----
        Points of a :class:`pyvista.RectilinearGrid` cannot be
        set. Set point coordinates with :attr:`RectilinearGrid.x`,
        :attr:`RectilinearGrid.y`, or :attr:`RectilinearGrid.z`.

        Examples
        --------
        >>> import numpy as np
        >>> import pyvista
        >>> xrng = np.arange(-10, 10, 10, dtype=float)
        >>> yrng = np.arange(-10, 10, 10, dtype=float)
        >>> zrng = np.arange(-10, 10, 10, dtype=float)
        >>> grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
        >>> grid.points
        array([[-10., -10., -10.],
               [  0., -10., -10.],
               [-10.,   0., -10.],
               [  0.,   0., -10.],
               [-10., -10.,   0.],
               [  0., -10.,   0.],
               [-10.,   0.,   0.],
               [  0.,   0.,   0.]])

        """
        xx, yy, zz = self.meshgrid
        return np.c_[xx.ravel(order='F'), yy.ravel(order='F'), zz.ravel(order='F')]

    @points.setter
    def points(self, points):
        """Raise an AttributeError.

        This setter overrides the base class's setter to ensure a user
        does not attempt to set them.
        """
        raise AttributeError(
            "The points cannot be set. The points of "
            "`RectilinearGrid` are defined in each axial direction. Please "
            "use the `x`, `y`, and `z` setters individually."
        )

    @property
    def x(self) -> np.ndarray:
        """Return or set the coordinates along the X-direction.

        Examples
        --------
        Return the x coordinates of a RectilinearGrid.

        >>> import numpy as np
        >>> import pyvista
        >>> xrng = np.arange(-10, 10, 10, dtype=float)
        >>> yrng = np.arange(-10, 10, 10, dtype=float)
        >>> zrng = np.arange(-10, 10, 10, dtype=float)
        >>> grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
        >>> grid.x
        array([-10.,   0.])

        Set the x coordinates of a RectilinearGrid.

        >>> grid.x = [-10.0, 0.0, 10.0]
        >>> grid.x
        array([-10.,   0.,  10.])

        """
        return helpers.convert_array(self.GetXCoordinates())

    @x.setter
    def x(self, coords: Sequence):
        """Set the coordinates along the X-direction."""
        self.SetXCoordinates(helpers.convert_array(coords))
        self._update_dimensions()
        self.Modified()

    @property
    def y(self) -> np.ndarray:
        """Return or set the coordinates along the Y-direction.

        Examples
        --------
        Return the y coordinates of a RectilinearGrid.

        >>> import numpy as np
        >>> import pyvista
        >>> xrng = np.arange(-10, 10, 10, dtype=float)
        >>> yrng = np.arange(-10, 10, 10, dtype=float)
        >>> zrng = np.arange(-10, 10, 10, dtype=float)
        >>> grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
        >>> grid.y
        array([-10.,   0.])

        Set the y coordinates of a RectilinearGrid.

        >>> grid.y = [-10.0, 0.0, 10.0]
        >>> grid.y
        array([-10.,   0.,  10.])

        """
        return helpers.convert_array(self.GetYCoordinates())

    @y.setter
    def y(self, coords: Sequence):
        """Set the coordinates along the Y-direction."""
        self.SetYCoordinates(helpers.convert_array(coords))
        self._update_dimensions()
        self.Modified()

    @property
    def z(self) -> np.ndarray:
        """Return or set the coordinates along the Z-direction.

        Examples
        --------
        Return the z coordinates of a RectilinearGrid.

        >>> import numpy as np
        >>> import pyvista
        >>> xrng = np.arange(-10, 10, 10, dtype=float)
        >>> yrng = np.arange(-10, 10, 10, dtype=float)
        >>> zrng = np.arange(-10, 10, 10, dtype=float)
        >>> grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
        >>> grid.z
        array([-10.,   0.])

        Set the z coordinates of a RectilinearGrid.

        >>> grid.z = [-10.0, 0.0, 10.0]
        >>> grid.z
        array([-10.,   0.,  10.])

        """
        return helpers.convert_array(self.GetZCoordinates())

    @z.setter
    def z(self, coords: Sequence):
        """Set the coordinates along the Z-direction."""
        self.SetZCoordinates(helpers.convert_array(coords))
        self._update_dimensions()
        self.Modified()

    @Grid.dimensions.setter  # type: ignore
    def dimensions(self, dims):
        """Do not let the dimensions of the RectilinearGrid be set."""
        raise AttributeError(
            "The dimensions of a `RectilinearGrid` are implicitly "
            "defined and thus cannot be set."
        )

    def cast_to_structured_grid(self) -> 'pyvista.StructuredGrid':
        """Cast this rectilinear grid to a structured grid.

        Returns
        -------
        pyvista.StructuredGrid
            This grid as a structured grid.

        """
        alg = _vtk.vtkRectilinearGridToPointSet()
        alg.SetInputData(self)
        alg.Update()
        return _get_output(alg)


class UniformGrid(_vtk.vtkImageData, Grid, UniformGridFilters):
    """Models datasets with uniform spacing in the three coordinate directions.

    Can be initialized in one of several ways:

    - Create empty grid
    - Initialize from a vtk.vtkImageData object
    - Initialize based on dimensions, cell spacing, and origin.

    .. versionchanged:: 0.33.0
        First argument must now be either a path or
        ``vtk.vtkImageData``. Use keyword arguments to specify the
        dimensions, spacing, and origin of the uniform grid.

    .. versionchanged:: 0.37.0
        The ``dims`` parameter has been renamed to ``dimensions``.

    Parameters
    ----------
    uinput : str, vtk.vtkImageData, pyvista.UniformGrid, optional
        Filename or dataset to initialize the uniform grid from.  If
        set, remainder of arguments are ignored.

    dimensions : sequence[int], optional
        Dimensions of the uniform grid.

    spacing : sequence[float], default: (1.0, 1.0, 1.0)
        Spacing of the uniform grid in each dimension. Must be positive.

    origin : sequence[float], default: (0.0, 0.0, 0.0)
        Origin of the uniform grid.

    deep : bool, default: False
        Whether to deep copy a ``vtk.vtkImageData`` object.  Keyword only.

    Examples
    --------
    Create an empty UniformGrid.

    >>> import pyvista
    >>> grid = pyvista.UniformGrid()

    Initialize from a ``vtk.vtkImageData`` object.

    >>> import vtk
    >>> vtkgrid = vtk.vtkImageData()
    >>> grid = pyvista.UniformGrid(vtkgrid)

    Initialize using just the grid dimensions and default
    spacing and origin. These must be keyword arguments.

    >>> grid = pyvista.UniformGrid(dimensions=(10, 10, 10))

    Initialize using dimensions and spacing.

    >>> grid = pyvista.UniformGrid(
    ...     dimensions=(10, 10, 10),
    ...     spacing=(2, 1, 5),
    ... )

    Initialize using dimensions, spacing, and an origin.

    >>> grid = pyvista.UniformGrid(
    ...     dimensions=(10, 10, 10),
    ...     spacing=(2, 1, 5),
    ...     origin=(10, 35, 50),
    ... )

    Initialize from another UniformGrid.

    >>> grid = pyvista.UniformGrid(
    ...     dimensions=(10, 10, 10),
    ...     spacing=(2, 1, 5),
    ...     origin=(10, 35, 50),
    ... )
    >>> grid_from_grid = pyvista.UniformGrid(grid)
    >>> grid_from_grid == grid
    True

    """

    _WRITERS = {'.vtk': _vtk.vtkDataSetWriter, '.vti': _vtk.vtkXMLImageDataWriter}

    def __init__(
        self,
        uinput=None,
        *args,
        dimensions=None,
        spacing=(1.0, 1.0, 1.0),
        origin=(0.0, 0.0, 0.0),
        deep=False,
        **kwargs,
    ):
        """Initialize the uniform grid."""
        super().__init__()

        # permit old behavior
        if isinstance(uinput, Sequence) and not isinstance(uinput, str):
            # Deprecated on v0.37.0, estimated removal on v0.40.0
            warnings.warn(
                "Behavior of pyvista.UniformGrid has changed. First argument must be "
                "either a ``vtk.vtkImageData`` or path.",
                PyVistaDeprecationWarning,
            )
            dimensions = uinput
            uinput = None

        if dimensions is None and 'dims' in kwargs:
            dimensions = kwargs.pop('dims')
            # Deprecated on v0.37.0, estimated removal on v0.40.0
            warnings.warn(
                '`dims` argument is deprecated. Please use `dimensions`.', PyVistaDeprecationWarning
            )
        assert_empty_kwargs(**kwargs)

        if args:
            # Deprecated on v0.37.0, estimated removal on v0.40.0
            warnings.warn(
                "Behavior of pyvista.UniformGrid has changed. Use keyword arguments "
                "to specify dimensions, spacing, and origin. For example:\n\n"
                "    >>> grid = pyvista.UniformGrid(\n"
                "    ...     dimensions=(10, 10, 10),\n"
                "    ...     spacing=(2, 1, 5),\n"
                "    ...     origin=(10, 35, 50),\n"
                "    ... )\n",
                PyVistaDeprecationWarning,
            )
            origin = args[0]
            if len(args) > 1:
                spacing = args[1]
            if len(args) > 2:
                raise ValueError(
                    "Too many additional arguments specified for UniformGrid. "
                    f"Accepts at most 2, and {len(args)} have been input."
                )

        # first argument must be either vtkImageData or a path
        if uinput is not None:
            if isinstance(uinput, _vtk.vtkImageData):
                if deep:
                    self.deep_copy(uinput)
                else:
                    self.shallow_copy(uinput)
            elif isinstance(uinput, (str, pathlib.Path)):
                self._from_file(uinput)
            else:
                raise TypeError(
                    "First argument, ``uinput`` must be either ``vtk.vtkImageData`` "
                    f"or a path, not {type(uinput)}.  Use keyword arguments to "
                    "specify dimensions, spacing, and origin. For example:\n\n"
                    "    >>> grid = pyvista.UniformGrid(\n"
                    "    ...     dimensions=(10, 10, 10),\n"
                    "    ...     spacing=(2, 1, 5),\n"
                    "    ...     origin=(10, 35, 50),\n"
                    "    ... )\n"
                )
        elif dimensions is not None:
            self._from_specs(dimensions, spacing, origin)

    def __repr__(self):
        """Return the default representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the default str representation."""
        return DataSet.__str__(self)

    def _from_specs(self, dims: Sequence[int], spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0)):
        """Create VTK image data directly from numpy arrays.

        A uniform grid is defined by the point spacings for each axis
        (uniform along each individual axis) and the number of points on each axis.
        These are relative to a specified origin (default is ``(0.0, 0.0, 0.0)``).

        Parameters
        ----------
        dims : tuple(int)
            Length 3 tuple of ints specifying how many points along each axis.

        spacing : sequence[float], default: (1.0, 1.0, 1.0)
            Length 3 tuple of floats/ints specifying the point spacings
            for each axis. Must be positive.

        origin : sequence[float], default: (0.0, 0.0, 0.0)
            Length 3 tuple of floats/ints specifying minimum value for each axis.

        """
        xn, yn, zn = dims[0], dims[1], dims[2]
        xo, yo, zo = origin[0], origin[1], origin[2]
        self.SetDimensions(xn, yn, zn)
        self.SetOrigin(xo, yo, zo)
        self.spacing = (spacing[0], spacing[1], spacing[2])

    @property  # type: ignore
    def points(self) -> np.ndarray:  # type: ignore
        """Build a copy of the implicitly defined points as a numpy array.

        Notes
        -----
        The ``points`` for a :class:`pyvista.UniformGrid` cannot be set.

        Examples
        --------
        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(2, 2, 2))
        >>> grid.points
        array([[0., 0., 0.],
               [1., 0., 0.],
               [0., 1., 0.],
               [1., 1., 0.],
               [0., 0., 1.],
               [1., 0., 1.],
               [0., 1., 1.],
               [1., 1., 1.]])

        """
        # Get grid dimensions
        nx, ny, nz = self.dimensions
        nx -= 1
        ny -= 1
        nz -= 1
        # get the points and convert to spacings
        dx, dy, dz = self.spacing
        # Now make the cell arrays
        ox, oy, oz = np.array(self.origin) + np.array(self.extent[::2])  # type: ignore
        x = np.insert(np.cumsum(np.full(nx, dx)), 0, 0.0) + ox
        y = np.insert(np.cumsum(np.full(ny, dy)), 0, 0.0) + oy
        z = np.insert(np.cumsum(np.full(nz, dz)), 0, 0.0) + oz
        xx, yy, zz = np.meshgrid(x, y, z, indexing='ij')
        return np.c_[xx.ravel(order='F'), yy.ravel(order='F'), zz.ravel(order='F')]

    @points.setter
    def points(self, points):
        """Points cannot be set.

        This setter overrides the base class's setter to ensure a user does not
        attempt to set them. See https://github.com/pyvista/pyvista/issues/713.

        """
        raise AttributeError(
            "The points cannot be set. The points of "
            "`UniformGrid`/`vtkImageData` are implicitly defined by the "
            "`origin`, `spacing`, and `dimensions` of the grid."
        )

    @property
    def x(self) -> np.ndarray:
        """Return all the X points.

        Examples
        --------
        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(2, 2, 2))
        >>> grid.x
        array([0., 1., 0., 1., 0., 1., 0., 1.])

        """
        return self.points[:, 0]

    @property
    def y(self) -> np.ndarray:
        """Return all the Y points.

        Examples
        --------
        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(2, 2, 2))
        >>> grid.y
        array([0., 0., 1., 1., 0., 0., 1., 1.])

        """
        return self.points[:, 1]

    @property
    def z(self) -> np.ndarray:
        """Return all the Z points.

        Examples
        --------
        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(2, 2, 2))
        >>> grid.z
        array([0., 0., 0., 0., 1., 1., 1., 1.])

        """
        return self.points[:, 2]

    @property
    def origin(self) -> Tuple[float]:
        """Return the origin of the grid (bottom southwest corner).

        Examples
        --------
        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(5, 5, 5))
        >>> grid.origin
        (0.0, 0.0, 0.0)

        Show how the origin is in the bottom "southwest" corner of the
        UniformGrid.

        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(grid, show_edges=True)
        >>> _ = pl.add_axes_at_origin(ylabel=None)
        >>> pl.camera_position = 'xz'
        >>> pl.show()

        Set the origin to ``(1, 1, 1)`` and show how this shifts the
        UniformGrid.

        >>> grid.origin = (1, 1, 1)
        >>> pl = pyvista.Plotter()
        >>> _ = pl.add_mesh(grid, show_edges=True)
        >>> _ = pl.add_axes_at_origin(ylabel=None)
        >>> pl.camera_position = 'xz'
        >>> pl.show()

        """
        return self.GetOrigin()

    @origin.setter
    def origin(self, origin: Sequence[Union[float, int]]):
        """Set the origin."""
        self.SetOrigin(origin[0], origin[1], origin[2])
        self.Modified()

    @property
    def spacing(self) -> Tuple[float, float, float]:
        """Return or set the spacing for each axial direction.

        Notes
        -----
        Spacing must be non-negative. While VTK accepts negative
        spacing, this results in unexpected behavior. See:
        https://github.com/pyvista/pyvista/issues/1967

        Examples
        --------
        Create a 5 x 5 x 5 uniform grid.

        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(5, 5, 5))
        >>> grid.spacing
        (1.0, 1.0, 1.0)
        >>> grid.plot(show_edges=True)

        Modify the spacing to ``(1, 2, 3)``

        >>> grid.spacing = (1, 2, 3)
        >>> grid.plot(show_edges=True)

        """
        return self.GetSpacing()

    @spacing.setter
    def spacing(self, spacing: Sequence[Union[float, int]]):
        """Set spacing."""
        if min(spacing) < 0:
            raise ValueError(f"Spacing must be non-negative, got {spacing}")
        self.SetSpacing(*spacing)
        self.Modified()

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = Grid._get_attrs(self)
        fmt = "{}, {}, {}".format(*[pyvista.FLOAT_FORMAT] * 3)
        attrs.append(("Spacing", self.spacing, fmt))
        return attrs

    def cast_to_structured_grid(self) -> 'pyvista.StructuredGrid':
        """Cast this uniform grid to a structured grid.

        Returns
        -------
        pyvista.StructuredGrid
            This grid as a structured grid.

        """
        alg = _vtk.vtkImageToStructuredGrid()
        alg.SetInputData(self)
        alg.Update()
        return _get_output(alg)

    def cast_to_rectilinear_grid(self) -> 'RectilinearGrid':
        """Cast this uniform grid to a rectilinear grid.

        Returns
        -------
        pyvista.RectilinearGrid
            This uniform grid as a rectilinear grid.

        """

        def gen_coords(i):
            coords = (
                np.cumsum(np.insert(np.full(self.dimensions[i] - 1, self.spacing[i]), 0, 0))
                + self.origin[i]
            )
            return coords

        xcoords = gen_coords(0)
        ycoords = gen_coords(1)
        zcoords = gen_coords(2)
        grid = pyvista.RectilinearGrid(xcoords, ycoords, zcoords)
        grid.point_data.update(self.point_data)
        grid.cell_data.update(self.cell_data)
        grid.field_data.update(self.field_data)
        grid.copy_meta_from(self, deep=True)
        return grid

    @property
    def extent(self) -> tuple:
        """Return or set the extent of the UniformGrid.

        The extent is simply the first and last indices for each of the three axes.

        Examples
        --------
        Create a ``UniformGrid`` and show its extent.

        >>> import pyvista
        >>> grid = pyvista.UniformGrid(dimensions=(10, 10, 10))
        >>> grid.extent
        (0, 9, 0, 9, 0, 9)

        >>> grid.extent = (2, 5, 2, 5, 2, 5)
        >>> grid.extent
        (2, 5, 2, 5, 2, 5)

        Note how this also modifies the grid bounds and dimensions. Since we
        use default spacing of 1 here, the bounds match the extent exactly.

        >>> grid.bounds
        (2.0, 5.0, 2.0, 5.0, 2.0, 5.0)
        >>> grid.dimensions
        (4, 4, 4)

        """
        return self.GetExtent()

    @extent.setter
    def extent(self, new_extent: Sequence[int]):
        """Set the extent of the UniformGrid."""
        if len(new_extent) != 6:
            raise ValueError('Extent must be a vector of 6 values.')
        self.SetExtent(new_extent)

    @wraps(RectilinearGridFilters.to_tetrahedra)
    def to_tetrahedra(self, *args, **kwargs):
        """Cast to a rectangular grid and then convert to tetrahedra."""
        return self.cast_to_rectilinear_grid().to_tetrahedra(*args, **kwargs)


# pyvista/core/objects.py

"""This module provides wrappers for vtkDataObjects.

The data objects does not have any sort of spatial reference.

"""
import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.utilities import FieldAssociation, get_array, row_array

from .dataset import DataObject
from .datasetattributes import DataSetAttributes


class Table(_vtk.vtkTable, DataObject):
    """Wrapper for the ``vtkTable`` class.

    Create by passing a 2D NumPy array of shape (``n_rows`` by ``n_columns``)
    or from a dictionary containing NumPy arrays.

    Examples
    --------
    >>> import pyvista as pv
    >>> import numpy as np
    >>> arrays = np.random.rand(100, 3)
    >>> table = pv.Table(arrays)

    """

    def __init__(self, *args, **kwargs):
        """Initialize the table."""
        super().__init__(*args, **kwargs)
        if len(args) == 1:
            if isinstance(args[0], _vtk.vtkTable):
                deep = kwargs.get('deep', True)
                if deep:
                    self.deep_copy(args[0])
                else:
                    self.shallow_copy(args[0])
            elif isinstance(args[0], (np.ndarray, list)):
                self._from_arrays(args[0])
            elif isinstance(args[0], dict):
                self._from_dict(args[0])
            elif 'pandas.core.frame.DataFrame' in str(type(args[0])):
                self._from_pandas(args[0])
            else:
                raise TypeError(f'Table unable to be made from ({type(args[0])})')

    @staticmethod
    def _prepare_arrays(arrays):
        arrays = np.asarray(arrays)
        if arrays.ndim == 1:
            return np.reshape(arrays, (1, -1))
        elif arrays.ndim == 2:
            return arrays.T
        else:
            raise ValueError('Only 1D or 2D arrays are supported by Tables.')

    def _from_arrays(self, arrays):
        np_table = self._prepare_arrays(arrays)
        for i, array in enumerate(np_table):
            self.row_arrays[f'Array {i}'] = array

    def _from_dict(self, array_dict):
        for array in array_dict.values():
            if not isinstance(array, np.ndarray) and array.ndim < 3:
                raise ValueError('Dictionary must contain only NumPy arrays with maximum of 2D.')
        for name, array in array_dict.items():
            self.row_arrays[name] = array

    def _from_pandas(self, data_frame):
        for name in data_frame.keys():
            self.row_arrays[name] = data_frame[name].values

    @property
    def n_rows(self):
        """Return the number of rows."""
        return self.GetNumberOfRows()

    @n_rows.setter
    def n_rows(self, n):
        """Set the number of rows."""
        self.SetNumberOfRows(n)

    @property
    def n_columns(self):
        """Return the number of columns."""
        return self.GetNumberOfColumns()

    @property
    def n_arrays(self):
        """Return the number of columns.

        Alias for: ``n_columns``.

        """
        return self.n_columns

    def _row_array(self, name=None):
        """Return row scalars of a vtk object.

        Parameters
        ----------
        name : str
            Name of row scalars to retrieve.

        Returns
        -------
        numpy.ndarray
            Numpy array of the row.

        """
        return self.row_arrays.get_array(name)

    @property
    def row_arrays(self):
        """Return the all row arrays."""
        return DataSetAttributes(
            vtkobject=self.GetRowData(), dataset=self, association=FieldAssociation.ROW
        )

    def keys(self):
        """Return the table keys.

        Returns
        -------
        list
            List of the array names of this table.

        """
        return self.row_arrays.keys()

    def items(self):
        """Return the table items.

        Returns
        -------
        list
            List containing tuples pairs of the name and array of the table arrays.

        """
        return self.row_arrays.items()

    def values(self):
        """Return the table values.

        Returns
        -------
        list
            List of the table arrays.

        """
        return self.row_arrays.values()

    def update(self, data):
        """Set the table data using a dict-like update.

        Parameters
        ----------
        data : DataSetAttributes
            Other dataset attributes to update from.

        """
        if isinstance(data, (np.ndarray, list)):
            # Allow table updates using array data
            data = self._prepare_arrays(data)
            data = {f'Array {i}': array for i, array in enumerate(data)}
        self.row_arrays.update(data)
        self.Modified()

    def pop(self, name):
        """Pop off an array by the specified name.

        Parameters
        ----------
        name : int or str
            Index or name of the row array.

        Returns
        -------
        pyvista.pyvista_ndarray
            PyVista array.

        """
        return self.row_arrays.pop(name)

    def __getitem__(self, index):
        """Search row data for an array."""
        return self._row_array(name=index)

    def _ipython_key_completions_(self):
        return self.keys()

    def get(self, index):
        """Get an array by its name.

        Parameters
        ----------
        index : int or str
            Index or name of the row.

        Returns
        -------
        pyvista.pyvista_ndarray
            PyVista array.
        """
        return self[index]

    def __setitem__(self, name, scalars):
        """Add/set an array in the row_arrays."""
        self.row_arrays[name] = scalars

    def _remove_array(self, field, key):
        """Remove a single array by name from each field (internal helper)."""
        self.row_arrays.remove(key)

    def __delitem__(self, name):
        """Remove an array by the specified name."""
        del self.row_arrays[name]

    def __iter__(self):
        """Return the iterator across all arrays."""
        for array_name in self.row_arrays:
            yield self.row_arrays[array_name]

    def _get_attrs(self):
        """Return the representation methods."""
        attrs = []
        attrs.append(("N Rows", self.n_rows, "{}"))
        return attrs

    def _repr_html_(self):
        """Return a pretty representation for Jupyter notebooks.

        It includes header details and information about all arrays.

        """
        fmt = ""
        if self.n_arrays > 0:
            fmt += "<table>"
            fmt += "<tr><th>Header</th><th>Data Arrays</th></tr>"
            fmt += "<tr><td>"
        # Get the header info
        fmt += self.head(display=False, html=True)
        # Fill out scalars arrays
        if self.n_arrays > 0:
            fmt += "</td><td>"
            fmt += "\n"
            fmt += "<table>\n"
            titles = ["Name", "Type", "N Comp", "Min", "Max"]
            fmt += "<tr>" + "".join([f"<th>{t}</th>" for t in titles]) + "</tr>\n"
            row = "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n"
            row = "<tr>" + "".join(["<td>{}</td>" for i in range(len(titles))]) + "</tr>\n"

            def format_array(key):
                """Format array information for printing (internal helper)."""
                arr = row_array(self, key)
                dl, dh = self.get_data_range(key)
                dl = pyvista.FLOAT_FORMAT.format(dl)
                dh = pyvista.FLOAT_FORMAT.format(dh)
                if arr.ndim > 1:
                    ncomp = arr.shape[1]
                else:
                    ncomp = 1
                return row.format(key, arr.dtype, ncomp, dl, dh)

            for i in range(self.n_arrays):
                key = self.GetRowData().GetArrayName(i)
                fmt += format_array(key)

            fmt += "</table>\n"
            fmt += "\n"
            fmt += "</td></tr> </table>"
        return fmt

    def __repr__(self):
        """Return the object representation."""
        return self.head(display=False, html=False)

    def __str__(self):
        """Return the object string representation."""
        return self.head(display=False, html=False)

    def to_pandas(self):
        """Create a Pandas DataFrame from this Table.

        Returns
        -------
        pandas.DataFrame
            This table represented as a pandas dataframe.

        """
        try:
            import pandas as pd
        except ImportError:  # pragma: no cover
            raise ImportError('Install ``pandas`` to use this feature.')
        data_frame = pd.DataFrame()
        for name, array in self.items():
            data_frame[name] = array
        return data_frame

    def save(self, *args, **kwargs):  # pragma: no cover
        """Save the table."""
        raise NotImplementedError(
            "Please use the `to_pandas` method and harness Pandas' wonderful file IO methods."
        )

    def get_data_range(self, arr=None, preference='row'):
        """Get the non-NaN min and max of a named array.

        Parameters
        ----------
        arr : str, numpy.ndarray, optional
            The name of the array to get the range. If ``None``, the active scalar
            is used.

        preference : str, optional
            When scalars is specified, this is the preferred array type
            to search for in the dataset.  Must be either ``'row'`` or
            ``'field'``.

        Returns
        -------
        tuple
            ``(min, max)`` of the array.

        """
        if arr is None:
            # use the first array in the row data
            arr = self.GetRowData().GetArrayName(0)
        if isinstance(arr, str):
            arr = get_array(self, arr, preference=preference)
        # If array has no tuples return a NaN range
        if arr is None or arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
            return (np.nan, np.nan)
        # Use the array range
        return np.nanmin(arr), np.nanmax(arr)


# pyvista/core/pointset.py

"""Sub-classes and wrappers for vtk.vtkPointSet."""
import collections.abc
from functools import wraps
import numbers
import os
import pathlib
from textwrap import dedent
from typing import Sequence, Tuple, Union

import numpy as np

import pyvista
from pyvista import _vtk
from pyvista.utilities import abstract_class
from pyvista.utilities.cells import CellArray, create_mixed_cells, get_mixed_cells, numpy_to_idarr

from .._typing import BoundsLike
from ..utilities.fileio import get_ext
from .celltype import CellType
from .dataset import DataSet
from .errors import (
    PointSetCellOperationError,
    PointSetDimensionReductionError,
    PointSetNotSupported,
    VTKVersionError,
)
from .filters import PolyDataFilters, StructuredGridFilters, UnstructuredGridFilters, _get_output

DEFAULT_INPLACE_WARNING = (
    'You did not specify a value for `inplace` and the default value will '
    'be changing to `False` in future versions for point-based meshes (e.g., '
    '`PolyData`). Please make sure you are not assuming this to be an inplace '
    'operation.'
)


class _PointSet(DataSet):
    """PyVista's equivalent of vtk.vtkPointSet.

    This holds methods common to PolyData and UnstructuredGrid.
    """

    _WRITERS = {".xyz": _vtk.vtkSimplePointsWriter}

    def center_of_mass(self, scalars_weight=False):
        """Return the coordinates for the center of mass of the mesh.

        Parameters
        ----------
        scalars_weight : bool, default: False
            Flag for using the mesh scalars as weights.

        Returns
        -------
        numpy.ndarray
            Coordinates for the center of mass.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Sphere(center=(1, 1, 1))
        >>> mesh.center_of_mass()
        array([1., 1., 1.])

        """
        alg = _vtk.vtkCenterOfMass()
        alg.SetInputDataObject(self)
        alg.SetUseScalarsAsWeights(scalars_weight)
        alg.Update()
        return np.array(alg.GetCenter())

    def shallow_copy(self, to_copy):
        """Create a shallow copy from a different dataset into this one.

        This method mutates this dataset and returns ``None``.

        Parameters
        ----------
        to_copy : pyvista.DataSet
            Data object to perform the shallow copy from.

        """
        # Set default points if needed
        if not to_copy.GetPoints():
            to_copy.SetPoints(_vtk.vtkPoints())
        DataSet.shallow_copy(self, to_copy)

    def remove_cells(self, ind, inplace=False):
        """Remove cells.

        Parameters
        ----------
        ind : sequence
            Cell indices to be removed.  The array can also be a
            boolean array of the same size as the number of cells.

        inplace : bool, default: False
            Whether to update the mesh in-place.

        Returns
        -------
        pyvista.DataSet
            Same type as the input, but with the specified cells
            removed.

        Examples
        --------
        Remove 20 cells from an unstructured grid.

        >>> from pyvista import examples
        >>> import pyvista
        >>> hex_mesh = pyvista.read(examples.hexbeamfile)
        >>> removed = hex_mesh.remove_cells(range(10, 20))
        >>> removed.plot(color='tan', show_edges=True, line_width=3)
        """
        if isinstance(ind, np.ndarray):
            if ind.dtype == np.bool_ and ind.size != self.n_cells:
                raise ValueError(
                    f'Boolean array size must match the number of cells ({self.n_cells})'
                )
        ghost_cells = np.zeros(self.n_cells, np.uint8)
        ghost_cells[ind] = _vtk.vtkDataSetAttributes.DUPLICATECELL

        if inplace:
            target = self
        else:
            target = self.copy()

        target.cell_data[_vtk.vtkDataSetAttributes.GhostArrayName()] = ghost_cells
        target.RemoveGhostCells()
        return target

    def points_to_double(self):
        """Convert the points datatype to double precision.

        Returns
        -------
        pyvista.PointSet
            Pointset with points in double precision.

        Notes
        -----
        This operates in place.

        Examples
        --------
        Create a mesh that has points of the type ``float32`` and
        convert the points to ``float64``.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.points.dtype
        dtype('float32')
        >>> _ = mesh.points_to_double()
        >>> mesh.points.dtype
        dtype('float64')

        """
        if self.points.dtype != np.double:
            self.points = self.points.astype(np.double)
        return self

    # todo: `transform_all_input_vectors` is not handled when modifying inplace
    def translate(
        self, xyz: Union[list, tuple, np.ndarray], transform_all_input_vectors=False, inplace=None
    ):
        """Translate the mesh.

        Parameters
        ----------
        xyz : array_like[float]
            Cartesian values to displace with. Length 3 array-like.

        transform_all_input_vectors : bool, default: False
            When ``True``, all input vectors are transformed. Otherwise, only
            the points, normals and active vectors are transformed. This is
            only valid when not updating in place.

        inplace : bool, optional
            Updates mesh in-place.

        Returns
        -------
        pyvista.PointSet
            Translated pointset.

        Examples
        --------
        Create a sphere and translate it by ``(2, 1, 2)``.

        >>> import pyvista
        >>> mesh = pyvista.Sphere()
        >>> mesh.center
        [0.0, 0.0, 0.0]
        >>> trans = mesh.translate((2, 1, 2), inplace=True)
        >>> trans.center
        [2.0, 1.0, 2.0]

        """
        if inplace:
            self.points += np.asarray(xyz)  # type: ignore
            return self
        return super().translate(
            xyz, transform_all_input_vectors=transform_all_input_vectors, inplace=inplace
        )


class PointSet(_vtk.vtkPointSet, _PointSet):
    """Concrete class for storing a set of points.

    This is a concrete class representing a set of points that specifies the
    interface for datasets that explicitly use "point" arrays to represent
    geometry. This class is useful for improving the performance of filters on
    point clouds, but not plotting.

    For further details see `VTK: vtkPointSet Details
    <https://vtk.org/doc/nightly/html/classvtkPointSet.html#details>`_.

    Parameters
    ----------
    var_inp : vtk.vtkPointSet, Sequence, optional
        Flexible input type.  Can be a ``vtk.vtkPointSet``, in which case
        this PointSet object will be copied if ``deep=True`` and will
        be a shallow copy if ``deep=False``.

        List, numpy array, or sequence containing point locations. Must be an
        ``(N, 3)`` array of points.

    deep : bool, default: False
        Whether to copy the input ``points``, or to create a PointSet from them
        without copying them.  Setting ``deep=True`` ensures that the original
        arrays can be modified outside the mesh without affecting the
        mesh.

    force_float : bool, default: True
        Casts the datatype to ``float32`` if points datatype is non-float.  Set
        this to ``False`` to allow non-float types, though this may lead to
        truncation of intermediate floats when transforming datasets.

    Notes
    -----
    This class requires ``vtk>=9.1.0``. This is an abstract class in
    ``vtk<9.1.0`` and cannot be instantiated.

    Examples
    --------
    Create a simple point cloud of 10 points from a numpy array.

    >>> import numpy as np
    >>> import pyvista
    >>> rng = np.random.default_rng()
    >>> points = rng.random((10, 3))
    >>> pset = pyvista.PointSet(points)

    Plot the pointset. Note: this casts to a :class:`pyvista.PolyData`
    internally when plotting.

    >>> pset.plot(point_size=10)

    """

    def __new__(cls, *args, **kwargs):
        """Construct a new PointSet object.

        Wrapping this is necessary for us to show an informative error
        message when the VTK version is too old, causing PointSet to be
        an abstract class. Since we inherit the ``__new__()`` method of
        ``vtk.vtkPointSet``, we would otherwise see a generic error about
        the class being abstract.

        """
        if pyvista.vtk_version_info < (9, 1, 0):
            raise VTKVersionError("pyvista.PointSet requires VTK >= 9.1.0")
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, var_inp=None, deep=False, force_float=True):
        """Initialize the pointset."""
        super().__init__()

        if var_inp is None:
            return
        elif isinstance(var_inp, _vtk.vtkPointSet):
            if deep:
                self.deep_copy(var_inp)
            else:
                self.shallow_copy(var_inp)
        else:
            self.SetPoints(pyvista.vtk_points(var_inp, deep=deep, force_float=force_float))

    def __repr__(self):
        """Return the standard representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the standard str representation."""
        return DataSet.__str__(self)

    def cast_to_polydata(self, deep=True):
        """Cast this dataset to polydata.

        Parameters
        ----------
        deep : bool, deep: True
            Whether to copy the pointset points, or to create a PolyData
            without copying them.  Setting ``deep=True`` ensures that the
            original arrays can be modified outside the PolyData without
            affecting the PolyData.

        Returns
        -------
        pyvista.PolyData
            PointSet cast to a ``pyvista.PolyData``.

        """
        pdata = PolyData(self.points, deep=deep)
        if deep:
            pdata.point_data.update(self.point_data)  # update performs deep copy
        else:
            for key, value in self.point_data.items():
                pdata.point_data[key] = value
        return pdata

    @wraps(DataSet.plot)  # type: ignore
    def plot(self, *args, **kwargs):
        """Cast to PolyData and plot."""
        pdata = self.cast_to_polydata(deep=False)
        kwargs.setdefault('style', 'points')
        return pdata.plot(*args, **kwargs)

    @wraps(PolyDataFilters.threshold)
    def threshold(self, *args, **kwargs):
        """Cast to PolyData and threshold.

        Need this because cell-wise operations fail for PointSets.
        """
        return self.cast_to_polydata(False).threshold(*args, **kwargs).cast_to_pointset()

    @wraps(PolyDataFilters.threshold_percent)
    def threshold_percent(self, *args, **kwargs):
        """Cast to PolyData and threshold.

        Need this because cell-wise operations fail for PointSets.
        """
        return self.cast_to_polydata(False).threshold_percent(*args, **kwargs).cast_to_pointset()

    @wraps(PolyDataFilters.explode)
    def explode(self, *args, **kwargs):
        """Cast to PolyData and explode.

        The explode filter relies on cells.

        """
        return self.cast_to_polydata(False).explode(*args, **kwargs).cast_to_pointset()

    @wraps(PolyDataFilters.delaunay_3d)
    def delaunay_3d(self, *args, **kwargs):
        """Cast to PolyData and run delaunay_3d."""
        return self.cast_to_polydata(False).delaunay_3d(*args, **kwargs)

    @property
    def area(self) -> float:
        """Return 0.0 since a PointSet has no area."""
        return 0.0

    @property
    def volume(self) -> float:
        """Return 0.0 since a PointSet has no volume."""
        return 0.0

    def contour(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetNotSupported(
            'Contour and other dimension reducing filters are not supported on PointSets'
        )

    def cell_data_to_point_data(self, *args, **kwargs):
        """Raise PointSets do not have cells."""
        raise PointSetNotSupported('PointSets contain no cells or cell data.')

    def point_data_to_cell_data(self, *args, **kwargs):
        """Raise PointSets do not have cells."""
        raise PointSetNotSupported('PointSets contain no cells or cell data.')

    def triangulate(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def decimate_boundary(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def find_cells_along_line(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def tessellate(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def slice(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetDimensionReductionError

    def slice_along_axis(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetDimensionReductionError

    def slice_along_line(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetDimensionReductionError

    def slice_implicit(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetDimensionReductionError

    def slice_orthogonal(self, *args, **kwargs):
        """Raise dimension reducing operations are not supported."""
        raise PointSetDimensionReductionError

    def shrink(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def separate_cells(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def remove_cells(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError

    def point_is_inside_cell(self, *args, **kwargs):
        """Raise cell operations are not supported."""
        raise PointSetCellOperationError


class PolyData(_vtk.vtkPolyData, _PointSet, PolyDataFilters):
    """Dataset consisting of surface geometry (e.g. vertices, lines, and polygons).

    Can be initialized in several ways:

    - Create an empty mesh
    - Initialize from a vtk.vtkPolyData
    - Using vertices
    - Using vertices and faces
    - From a file

    Parameters
    ----------
    var_inp : vtk.vtkPolyData, str, sequence, optional
        Flexible input type.  Can be a ``vtk.vtkPolyData``, in which case
        this PolyData object will be copied if ``deep=True`` and will
        be a shallow copy if ``deep=False``.

        Also accepts a path, which may be local path as in
        ``'my_mesh.stl'`` or global path like ``'/tmp/my_mesh.ply'``
        or ``'C:/Users/user/my_mesh.ply'``.

        Otherwise, this must be a points array or list containing one
        or more points.  Each point must have 3 dimensions.

    faces : sequence, optional
        Face connectivity array.  Faces must contain padding
        indicating the number of points in the face.  For example, the
        two faces ``[10, 11, 12]`` and ``[20, 21, 22, 23]`` will be
        represented as ``[3, 10, 11, 12, 4, 20, 21, 22, 23]``.  This
        lets you have an arbitrary number of points per face.

        When not including the face connectivity array, each point
        will be assigned to a single vertex.  This is used for point
        clouds that have no connectivity.

    n_faces : int, optional
        Number of faces in the ``faces`` connectivity array.  While
        optional, setting this speeds up the creation of the
        ``PolyData``.

    lines : sequence, optional
        The line connectivity array.  Like ``faces``, this array
        requires padding indicating the number of points in a line
        segment.  For example, the two line segments ``[0, 1]`` and
        ``[1, 2, 3, 4]`` will be represented as
        ``[2, 0, 1, 4, 1, 2, 3, 4]``.

    n_lines : int, optional
        Number of lines in the ``lines`` connectivity array.  While
        optional, setting this speeds up the creation of the
        ``PolyData``.

    strips : sequence, optional
        Triangle strips connectivity array.  Triangle strips require an initial
        triangle, and the following points of the strip. Each
        triangle is built with the new point and the two previous
        points. Just as in ``lines`` and ``faces``, this array requires a
        padding indicating the number of points. For example,
        a single triangle strip of ``[0, 1, 2, 3, 6, 7, 4, 5, 0, 1]`` requires padding of
        ``10`` and should input as ``[10, 0, 1, 2, 3, 6, 7, 4, 5, 0, 1]``.

    n_strips : int, optional
        Number of strips in the ``strips`` connectivity array.  While
        optional, setting this speeds up the creation of the
        ``PolyData``.

    deep : bool, optional
        Whether to copy the inputs, or to create a mesh from them
        without copying them.  Setting ``deep=True`` ensures that the
        original arrays can be modified outside the mesh without
        affecting the mesh. Default is ``False``.

    force_ext : str, optional
        If initializing from a file, force the reader to treat the
        file as if it had this extension as opposed to the one in the
        file.

    force_float : bool, optional
        Casts the datatype to ``float32`` if points datatype is
        non-float.  Default ``True``. Set this to ``False`` to allow
        non-float types, though this may lead to truncation of
        intermediate floats when transforming datasets.

    Examples
    --------
    >>> import vtk
    >>> import numpy as np
    >>> from pyvista import examples
    >>> import pyvista

    Create an empty mesh.

    >>> mesh = pyvista.PolyData()

    Initialize from a ``vtk.vtkPolyData`` object.

    >>> vtkobj = vtk.vtkPolyData()
    >>> mesh = pyvista.PolyData(vtkobj)

    Initialize from just vertices.

    >>> vertices = np.array(
    ...     [[0, 0, 0], [1, 0, 0], [1, 0.5, 0], [0, 0.5, 0]]
    ... )
    >>> mesh = pyvista.PolyData(vertices)

    Initialize from vertices and faces.

    >>> faces = np.hstack([[3, 0, 1, 2], [3, 0, 3, 2]])
    >>> mesh = pyvista.PolyData(vertices, faces)

    Initialize from vertices and lines.

    >>> lines = np.hstack([[2, 0, 1], [2, 1, 2]])
    >>> mesh = pyvista.PolyData(vertices, lines=lines)

    Initialize from vertices and triangle strips.

    >>> strips = np.hstack([[4, 0, 1, 3, 2]])
    >>> mesh = pyvista.PolyData(vertices, strips=strips)

    Initialize from a filename.

    >>> mesh = pyvista.PolyData(examples.antfile)

    See :ref:`ref_create_poly` for more examples.

    """

    _WRITERS = {
        '.ply': _vtk.vtkPLYWriter,
        '.vtp': _vtk.vtkXMLPolyDataWriter,
        '.stl': _vtk.vtkSTLWriter,
        '.vtk': _vtk.vtkPolyDataWriter,
    }

    def __init__(
        self,
        var_inp=None,
        faces=None,
        n_faces=None,
        lines=None,
        n_lines=None,
        strips=None,
        n_strips=None,
        deep=False,
        force_ext=None,
        force_float=True,
    ) -> None:
        """Initialize the polydata."""
        local_parms = locals()
        super().__init__()

        # allow empty input
        if var_inp is None:
            return

        # filename
        opt_kwarg = ['faces', 'n_faces', 'lines', 'n_lines']
        if isinstance(var_inp, (str, pathlib.Path)):
            for kwarg in opt_kwarg:
                if local_parms[kwarg]:
                    raise ValueError(
                        'No other arguments should be set when first parameter is a string'
                    )
            self._from_file(var_inp, force_ext=force_ext)  # is filename

            return

        # PolyData-like
        if isinstance(var_inp, _vtk.vtkPolyData):
            for kwarg in opt_kwarg:
                if local_parms[kwarg]:
                    raise ValueError(
                        'No other arguments should be set when first parameter is a PolyData'
                    )
            if deep:
                self.deep_copy(var_inp)
            else:
                self.shallow_copy(var_inp)
            return

        # First parameter is points
        if isinstance(var_inp, (np.ndarray, list, _vtk.vtkDataArray)):
            self.SetPoints(pyvista.vtk_points(var_inp, deep=deep, force_float=force_float))

        else:
            msg = f"""
                Invalid Input type:

                Expected first argument to be either a:
                - vtk.PolyData
                - pyvista.PolyData
                - numeric numpy.ndarray (1 or 2 dimensions)
                - List (flat or nested with 3 points per vertex)
                - vtk.vtkDataArray

                Instead got: {type(var_inp)}"""
            raise TypeError(dedent(msg.strip('\n')))

        # At this point, points have been setup, add faces and/or lines
        if faces is None and lines is None and strips is None:
            # one cell per point (point cloud case)
            verts = self._make_vertex_cells(self.n_points)
            self.verts = CellArray(verts, self.n_points, deep)
        elif strips is not None:
            self.strips = CellArray(strips, n_strips, deep)
        elif faces is not None:
            # here we use CellArray since we must specify deep and n_faces
            self.faces = CellArray(faces, n_faces, deep)

        # can always set lines
        if lines is not None:
            # here we use CellArray since we must specify deep and n_lines
            self.lines = CellArray(lines, n_lines, deep)

    def _post_file_load_processing(self):
        """Execute after loading a PolyData from file."""
        # When loading files with just point arrays, create and
        # set the polydata vertices
        if self.n_points > 0 and self.n_cells == 0:
            verts = self._make_vertex_cells(self.n_points)
            self.verts = CellArray(verts, self.n_points, deep=False)

    def __repr__(self):
        """Return the standard representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the standard str representation."""
        return DataSet.__str__(self)

    @staticmethod
    def _make_vertex_cells(npoints):
        cells = np.empty((npoints, 2), dtype=pyvista.ID_TYPE)
        cells[:, 0] = 1
        cells[:, 1] = np.arange(npoints, dtype=pyvista.ID_TYPE)
        return cells

    @property
    def verts(self) -> np.ndarray:
        """Get the vertex cells.

        Returns
        -------
        numpy.ndarray
            Array of vertex cell indices.

        Examples
        --------
        Create a point cloud polydata and return the vertex cells.

        >>> import pyvista
        >>> import numpy as np
        >>> points = np.random.random((5, 3))
        >>> pdata = pyvista.PolyData(points)
        >>> pdata.verts
        array([1, 0, 1, 1, 1, 2, 1, 3, 1, 4])

        Set vertex cells.  Note how the mesh plots both the surface
        mesh and the additional vertices in a single plot.

        >>> mesh = pyvista.Plane(i_resolution=3, j_resolution=3)
        >>> mesh.verts = np.vstack(
        ...     (
        ...         np.ones(mesh.n_points, dtype=np.int64),
        ...         np.arange(mesh.n_points),
        ...     )
        ... ).T
        >>> mesh.plot(
        ...     color='tan',
        ...     render_points_as_spheres=True,
        ...     point_size=60,
        ... )

        """
        return _vtk.vtk_to_numpy(self.GetVerts().GetData())

    @verts.setter
    def verts(self, verts):
        """Set the vertex cells."""
        if isinstance(verts, CellArray):
            self.SetVerts(verts)
        else:
            self.SetVerts(CellArray(verts))

    @property
    def lines(self) -> np.ndarray:
        """Return a pointer to the lines as a numpy array.

        Examples
        --------
        Return the lines from a spline.

        >>> import pyvista
        >>> import numpy as np
        >>> points = np.random.random((3, 3))
        >>> spline = pyvista.Spline(points, 10)
        >>> spline.lines
        array([10,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9])

        """
        return _vtk.vtk_to_numpy(self.GetLines().GetData()).ravel()

    @lines.setter
    def lines(self, lines):
        """Set the lines of the polydata."""
        if isinstance(lines, CellArray):
            self.SetLines(lines)
        else:
            self.SetLines(CellArray(lines))

    @property
    def faces(self) -> np.ndarray:
        """Return the connectivity array of the faces of this PolyData.

        The faces array is organized as::

           [n0, p0_0, p0_1, ..., p0_n, n1, p1_0, p1_1, ..., p1_n, ...]

        where ``n0`` is the number of points in face 0, and ``pX_Y`` is the
        Y'th point in face X.

        For example, a triangle and a quadrilateral might be represented as::

           [3, 0, 1, 2, 4, 0, 1, 3, 4]

        Where the two individual faces would be ``[3, 0, 1, 2]`` and ``[4, 0, 1, 3, 4]``.

        Returns
        -------
        numpy.ndarray
            Array of face connectivity.

        Notes
        -----
        The array returned cannot be modified in place and will raise a
        ``ValueError`` if attempted.

        You can, however, set the faces directly. See the example.

        Examples
        --------
        >>> import pyvista as pv
        >>> plane = pv.Plane(i_resolution=2, j_resolution=2)
        >>> plane.faces
        array([4, 0, 1, 4, 3, 4, 1, 2, 5, 4, 4, 3, 4, 7, 6, 4, 4, 5, 8, 7])

        Note how the faces contain a "padding" indicating the number
        of points per face:

        >>> plane.faces.reshape(-1, 5)
        array([[4, 0, 1, 4, 3],
               [4, 1, 2, 5, 4],
               [4, 3, 4, 7, 6],
               [4, 4, 5, 8, 7]])

        Set the faces directly. The following example creates a simple plane
        with a single square faces and modifies it to have two triangles
        instead.

        >>> mesh = pv.Plane(i_resolution=1, j_resolution=1)
        >>> mesh.faces = [3, 0, 1, 2, 3, 3, 2, 1]
        >>> mesh.faces
        array([3, 0, 1, 2, 3, 3, 2, 1])

        """
        array = _vtk.vtk_to_numpy(self.GetPolys().GetData())
        # Flag this array as read only to ensure users do not attempt to write to it.
        array.flags['WRITEABLE'] = False
        return array

    @faces.setter
    def faces(self, faces):
        """Set the face cells."""
        if isinstance(faces, CellArray):
            self.SetPolys(faces)
        else:
            # TODO: faster to mutate in-place if array is same size?
            self.SetPolys(CellArray(faces))

    @property
    def strips(self) -> np.ndarray:
        """Return a pointer to the strips as a numpy array.

        Returns
        -------
        numpy.ndarray
            Array of strip indices.

        Examples
        --------
        >>> import pyvista as pv
        >>> polygon = pv.Rectangle()
        >>> extruded = polygon.extrude((0, 0, 1), capping=False)
        >>> extruded.strips
        array([4, 0, 1, 4, 5, 4, 1, 2, 5, 6, 4, 2, 3, 6, 7, 4, 3, 0, 7, 4])
        """
        return _vtk.vtk_to_numpy(self.GetStrips().GetData())

    @strips.setter
    def strips(self, strips):
        """Set the strip cells."""
        if isinstance(strips, CellArray):
            self.SetStrips(strips)
        else:
            self.SetStrips(CellArray(strips))

    @property
    def is_all_triangles(self):
        """Return if all the faces of the :class:`pyvista.PolyData` are triangles.

        Returns
        -------
        bool
            ``True`` if all the faces of the :class:`pyvista.PolyData`
            are triangles and does not contain any vertices or lines.

        Examples
        --------
        Show a mesh from :func:`pyvista.Plane` is not composed of all
        triangles.

        >>> import pyvista
        >>> plane = pyvista.Plane()
        >>> plane.is_all_triangles
        False

        Show that the mesh from :func:`pyvista.Sphere` contains only
        triangles.

        >>> sphere = pyvista.Sphere()
        >>> sphere.is_all_triangles
        True

        """
        # Need to make sure there are only face cells and no lines/verts
        if not self.n_faces or self.n_lines or self.n_verts:
            return False

        # early return if not all triangular
        if self._connectivity_array.size % 3:
            return False

        # next, check if there are three points per face
        return (np.diff(self._offset_array) == 3).all()

    def __sub__(self, cutting_mesh):
        """Compute boolean difference of two meshes."""
        return self.boolean_difference(cutting_mesh)

    @property
    def _offset_array(self):
        """Return the array used to store cell offsets."""
        return _vtk.vtk_to_numpy(self.GetPolys().GetOffsetsArray())

    @property
    def _connectivity_array(self):
        """Return the array with the point ids that define the cells connectivity."""
        try:
            return _vtk.vtk_to_numpy(self.GetPolys().GetConnectivityArray())
        except AttributeError:  # pragma: no cover
            raise VTKVersionError('Connectivity array implemented in VTK 9 or newer.')

    @property
    def n_lines(self) -> int:
        """Return the number of lines.

        Examples
        --------
        >>> import pyvista
        >>> mesh = pyvista.Line()
        >>> mesh.n_lines
        1

        """
        return self.GetNumberOfLines()

    @property
    def n_verts(self) -> int:
        """Return the number of vertices.

        Examples
        --------
        Create a simple mesh containing just two points and return the
        number of vertices.

        >>> import pyvista
        >>> mesh = pyvista.PolyData([[1.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
        >>> mesh.n_verts
        2

        """
        return self.GetNumberOfVerts()

    @property
    def n_strips(self) -> int:
        """Return the number of strips.

        Examples
        --------
        Create a simple mesh with one triangle strip and return the
        number of triangles.

        >>> import pyvista
        >>> import numpy as np
        >>> vertices = np.array(
        ...     [[1.0, 0.0, 0.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]
        ... )
        >>> strip = np.array([3, 0, 1, 2])
        >>> mesh = pyvista.PolyData(vertices, strips=strip)
        >>> mesh.n_strips
        1

        """
        return self.GetNumberOfStrips()

    @property
    def n_faces(self) -> int:
        """Return the number of cells.

        Alias for ``n_cells``.

        Examples
        --------
        >>> import pyvista
        >>> plane = pyvista.Plane(i_resolution=2, j_resolution=2)
        >>> plane.n_faces
        4

        """
        return self.n_cells

    def save(self, filename, binary=True, texture=None, recompute_normals=True):
        """Write a surface mesh to disk.

        Written file may be an ASCII or binary ply, stl, or vtk mesh
        file.

        Parameters
        ----------
        filename : str
            Filename of mesh to be written.  File type is inferred from
            the extension of the filename unless overridden with
            ftype.  Can be one of many of the supported  the following
            types (``'.ply'``, ``'.stl'``, ``'.vtk``).

        binary : bool, default: True
            Writes the file as binary when ``True`` and ASCII when ``False``.

        texture : str, numpy.ndarray, optional
            Write a single texture array to file when using a PLY
            file.  Texture array must be a 3 or 4 component array with
            the datatype ``np.uint8``.  Array may be a cell array or a
            point array, and may also be a string if the array already
            exists in the PolyData.

            If a string is provided, the texture array will be saved
            to disk as that name.  If an array is provided, the
            texture array will be saved as ``'RGBA'`` if the array
            contains an alpha channel (i.e. 4 component array), or
            as ``'RGB'`` if the array is just a 3 component array.

            .. note::
               This feature is only available when saving PLY files.

        recompute_normals : bool, default: True
            When ``True``, if ply or stl format is chosen, the face normals
            are computed in place to ensure the mesh is properly saved.
            Set this to ``False`` to save instead the already existing normal
            array in the PolyData.

        Notes
        -----
        Binary files write much faster than ASCII and have a smaller
        file size.

        Examples
        --------
        Save a mesh as a STL.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.save('my_mesh.stl')  # doctest:+SKIP

        Save a mesh as a PLY.

        >>> sphere = pyvista.Sphere()
        >>> sphere.save('my_mesh.ply')  # doctest:+SKIP

        Save a mesh as a PLY with a texture array.  Here we also
        create a simple RGB array representing the texture.

        >>> import numpy as np
        >>> sphere = pyvista.Sphere()
        >>> texture = np.zeros((sphere.n_points, 3), np.uint8)
        >>> # Just the green channel is set as a repeatedly
        >>> # decreasing value
        >>> texture[:, 1] = np.arange(sphere.n_points)[::-1]
        >>> sphere.point_data['my_texture'] = texture
        >>> sphere.save(
        ...     'my_mesh.ply', texture='my_texture'
        ... )  # doctest:+SKIP

        Alternatively, provide just the texture array.  This will be
        written to the file as ``'RGB'`` since it does not contain an
        alpha channel.

        >>> sphere.save('my_mesh.ply', texture=texture)  # doctest:+SKIP

        Save a mesh as a VTK file.

        >>> sphere = pyvista.Sphere()
        >>> sphere.save('my_mesh.vtk')  # doctest:+SKIP

        """
        filename = os.path.abspath(os.path.expanduser(str(filename)))
        ftype = get_ext(filename)
        # Recompute normals prior to save.  Corrects a bug were some
        # triangular meshes are not saved correctly
        if ftype in ['.stl', '.ply'] and recompute_normals:
            self.compute_normals(inplace=True)

        # validate texture
        if ftype == '.ply' and texture is not None:
            if isinstance(texture, str):
                if self[texture].dtype != np.uint8:
                    raise ValueError(
                        f'Invalid datatype {self[texture].dtype} of texture array "{texture}"'
                    )
            elif isinstance(texture, np.ndarray):
                if texture.dtype != np.uint8:
                    raise ValueError(f'Invalid datatype {texture.dtype} of texture array')
            else:
                raise TypeError(
                    f'Invalid type {type(texture)} for texture.  '
                    'Should be either a string representing a point or '
                    'cell array, or a numpy array.'
                )

        super().save(filename, binary, texture=texture)

    @property
    def volume(self) -> float:
        """Return the approximate volume of the dataset.

        This will throw a VTK error/warning if not a closed surface.

        Returns
        -------
        float
            Total volume of the mesh.

        Examples
        --------
        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.volume
        0.5183

        """
        mprop = _vtk.vtkMassProperties()
        mprop.SetInputData(self.triangulate())
        return mprop.GetVolume()

    @property
    def point_normals(self) -> 'pyvista.pyvista_ndarray':
        """Return the point normals.

        If the point data already contains an array named ``'Normals'``, this
        array will be returned. Otherwise, the normals will be computed using
        the default options of :func:`compute_normals()
        <pyvista.PolyDataFilters.compute_normals>` and returned.

        Returns
        -------
        pyvista.pyvista_ndarray
            Array of point normals.

        Examples
        --------
        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.point_normals  # doctest:+SKIP
        pyvista_ndarray([[-2.48721432e-10, -1.08815623e-09, -1.00000000e+00],
                         [-2.48721432e-10, -1.08815623e-09,  1.00000000e+00],
                         [-1.18888125e-01,  3.40539310e-03, -9.92901802e-01],
                         ...,
                         [-3.11940581e-01, -6.81432486e-02,  9.47654784e-01],
                         [-2.09880397e-01, -4.65070531e-02,  9.76620376e-01],
                         [-1.15582108e-01, -2.80492082e-02,  9.92901802e-01]],
                        dtype=float32)

        """
        if 'Normals' in self.point_data:
            normals = self.point_data['Normals']
        else:
            normals = self.compute_normals(cell_normals=False, inplace=False).point_data['Normals']
        return normals

    @property
    def cell_normals(self) -> 'pyvista.pyvista_ndarray':
        """Return the cell normals.

        If the cell data already contains an array named ``'Normals'``, this
        array will be returned. Otherwise, the normals will be computed using
        the default options of :func:`compute_normals()
        <pyvista.PolyDataFilters.compute_normals>` and returned.

        Returns
        -------
        pyvista.pyvista_ndarray
            Array of cell normals.

        Examples
        --------
        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.cell_normals  # doctest:+SKIP
        pyvista_ndarray([[-0.05413816,  0.00569015, -0.9985172 ],
                         [-0.05177207,  0.01682176, -0.9985172 ],
                         [-0.04714328,  0.02721819, -0.9985172 ],
                         ...,
                         [-0.26742265, -0.02810723,  0.96316934],
                         [-0.1617585 , -0.01700151,  0.9866839 ],
                         [-0.1617585 , -0.01700151,  0.9866839 ]], dtype=float32)

        """
        if 'Normals' in self.cell_data:
            normals = self.cell_data['Normals']
        else:
            normals = self.compute_normals(point_normals=False, inplace=False).cell_data['Normals']
        return normals

    @property
    def face_normals(self) -> 'pyvista.pyvista_ndarray':
        """Return the cell normals.

        Alias to :func:`PolyData.cell_normals`.

        Returns
        -------
        pyvista.pyvista_ndarray
            Array of face normals.

        Examples
        --------
        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.face_normals  # doctest:+SKIP
        pyvista_ndarray([[-0.05413816,  0.00569015, -0.9985172 ],
                         [-0.05177207,  0.01682176, -0.9985172 ],
                         [-0.04714328,  0.02721819, -0.9985172 ],
                         ...,
                         [-0.26742265, -0.02810723,  0.96316934],
                         [-0.1617585 , -0.01700151,  0.9866839 ],
                         [-0.1617585 , -0.01700151,  0.9866839 ]], dtype=float32)

        """
        return self.cell_normals

    @property
    def obbTree(self):
        """Return the obbTree of the polydata.

        An obbTree is an object to generate oriented bounding box (OBB)
        trees. An oriented bounding box is a bounding box that does not
        necessarily line up along coordinate axes. The OBB tree is a
        hierarchical tree structure of such boxes, where deeper levels of OBB
        confine smaller regions of space.
        """
        if not hasattr(self, '_obbTree'):
            self._obbTree = _vtk.vtkOBBTree()
            self._obbTree.SetDataSet(self)
            self._obbTree.BuildLocator()

        return self._obbTree

    @property
    def n_open_edges(self) -> int:
        """Return the number of open edges on this mesh.

        Examples
        --------
        Return the number of open edges on a sphere.

        >>> import pyvista
        >>> sphere = pyvista.Sphere()
        >>> sphere.n_open_edges
        0

        Return the number of open edges on a plane.

        >>> plane = pyvista.Plane(i_resolution=1, j_resolution=1)
        >>> plane.n_open_edges
        4

        """
        alg = _vtk.vtkFeatureEdges()
        alg.FeatureEdgesOff()
        alg.BoundaryEdgesOn()
        alg.NonManifoldEdgesOn()
        alg.SetInputDataObject(self)
        alg.Update()
        return alg.GetOutput().GetNumberOfCells()

    @property
    def is_manifold(self) -> bool:
        """Return if the mesh is manifold (no open edges).

        Examples
        --------
        Show a sphere is manifold.

        >>> import pyvista
        >>> pyvista.Sphere().is_manifold
        True

        Show a plane is not manifold.

        >>> pyvista.Plane().is_manifold
        False

        """
        return self.n_open_edges == 0

    def __del__(self):
        """Delete the object."""
        if hasattr(self, '_obbTree'):
            del self._obbTree


@abstract_class
class PointGrid(_PointSet):
    """Class in common with structured and unstructured grids."""

    def __init__(self, *args, **kwargs) -> None:
        """Initialize the point grid."""
        super().__init__()

    def plot_curvature(self, curv_type='mean', **kwargs):
        """Plot the curvature of the external surface of the grid.

        Parameters
        ----------
        curv_type : str, default: "mean"
            One of the following strings indicating curvature types.
            - ``'mean'``
            - ``'gaussian'``
            - ``'maximum'``
            - ``'minimum'``

        **kwargs : dict, optional
            Optional keyword arguments.  See :func:`pyvista.plot`.

        Returns
        -------
        list
            Camera position, focal point, and view up.  Returned when
            ``return_cpos`` is ``True``.

        """
        trisurf = self.extract_surface().triangulate()
        return trisurf.plot_curvature(curv_type, **kwargs)


class UnstructuredGrid(_vtk.vtkUnstructuredGrid, PointGrid, UnstructuredGridFilters):
    """Dataset used for arbitrary combinations of all possible cell types.

    Can be initialized by the following:

    - Creating an empty grid
    - From a ``vtk.vtkPolyData`` or ``vtk.vtkStructuredGrid`` object
    - From cell, cell types, and point arrays
    - From a file

    Parameters
    ----------
    args : str, vtk.vtkUnstructuredGrid, iterable
        See examples below.
    deep : bool, default: False
        Whether to deep copy a vtkUnstructuredGrid object.
        Default is ``False``.  Keyword only.

    Examples
    --------
    >>> import pyvista
    >>> from pyvista import examples
    >>> import vtk

    Create an empty grid

    >>> grid = pyvista.UnstructuredGrid()

    Copy a vtk.vtkUnstructuredGrid

    >>> vtkgrid = vtk.vtkUnstructuredGrid()
    >>> grid = pyvista.UnstructuredGrid(vtkgrid)

    From a filename.

    >>> grid = pyvista.UnstructuredGrid(examples.hexbeamfile)
    >>> grid.plot(show_edges=True)

    From arrays. Here we create a single tetrahedron.

    >>> cells = [4, 0, 1, 2, 3]
    >>> celltypes = [pyvista.CellType.TETRA]
    >>> points = [
    ...     [1.0, 1.0, 1.0],
    ...     [1.0, -1.0, -1.0],
    ...     [-1.0, 1.0, -1.0],
    ...     [-1.0, -1.0, 1.0],
    ... ]
    >>> grid = pyvista.UnstructuredGrid(cells, celltypes, points)
    >>> grid.plot(show_edges=True)

    See the :ref:`create_unstructured_example` example for more details
    on creating unstructured grids within PyVista.

    """

    _WRITERS = {'.vtu': _vtk.vtkXMLUnstructuredGridWriter, '.vtk': _vtk.vtkUnstructuredGridWriter}

    def __init__(self, *args, deep=False, **kwargs) -> None:
        """Initialize the unstructured grid."""
        super().__init__()

        if not len(args):
            return
        if len(args) == 1:
            if isinstance(args[0], _vtk.vtkUnstructuredGrid):
                if deep:
                    self.deep_copy(args[0])
                else:
                    self.shallow_copy(args[0])

            elif isinstance(args[0], (str, pathlib.Path)):
                self._from_file(args[0], **kwargs)

            elif isinstance(args[0], (_vtk.vtkStructuredGrid, _vtk.vtkPolyData)):
                vtkappend = _vtk.vtkAppendFilter()
                vtkappend.AddInputData(args[0])
                vtkappend.Update()
                self.shallow_copy(vtkappend.GetOutput())

            else:
                itype = type(args[0])
                raise TypeError(f'Cannot work with input type {itype}')

        # Cell dictionary creation
        elif len(args) == 2 and isinstance(args[0], dict) and isinstance(args[1], np.ndarray):
            self._from_cells_dict(args[0], args[1], deep)
            self._check_for_consistency()

        elif len(args) == 3:
            arg0_is_seq = isinstance(args[0], (np.ndarray, collections.abc.Sequence))
            arg1_is_seq = isinstance(args[1], (np.ndarray, collections.abc.Sequence))
            arg2_is_seq = isinstance(args[2], (np.ndarray, collections.abc.Sequence))

            if all([arg0_is_seq, arg1_is_seq, arg2_is_seq]):
                self._from_arrays(args[0], args[1], args[2], deep, **kwargs)
                self._check_for_consistency()
            else:
                raise TypeError('All input types must be sequences.')
        else:
            raise TypeError(
                'Invalid parameters.  Initialization with arrays requires the '
                'following arrays:\n`cells`, `cell_type`, `points`'
            )

    def __repr__(self):
        """Return the standard representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the standard str representation."""
        return DataSet.__str__(self)

    def _from_cells_dict(self, cells_dict, points, deep=True):
        if points.ndim != 2 or points.shape[-1] != 3:
            raise ValueError("Points array must be a [M, 3] array")

        nr_points = points.shape[0]
        cell_types, cells = create_mixed_cells(cells_dict, nr_points)
        self._from_arrays(cells, cell_types, points, deep=deep)

    def _from_arrays(
        self,
        cells,
        cell_type,
        points,
        deep=True,
        force_float=True,
    ):
        """Create VTK unstructured grid from numpy arrays.

        Parameters
        ----------
        cells : sequence[int]
            Array of cells.  Each cell contains the number of points in the
            cell and the node numbers of the cell.

        cell_type : sequence[int]
            Cell types of each cell.  Each cell type numbers can be found from
            vtk documentation.  More efficient if using ``np.uint8``. See
            example below.

        points : sequence[float]
            Numpy array containing point locations.

        deep : bool, default: True
            When ``True``, makes a copy of the points array.  Default
            ``False``.  Cells and cell types are always copied.

        force_float : bool, default: True
            Casts the datatype to ``float32`` if points datatype is
            non-float.  Set this to ``False`` to allow non-float types,
            though this may lead to truncation of intermediate floats when
            transforming datasets.

        Examples
        --------
        >>> import numpy as np
        >>> from pyvista import CellType
        >>> import pyvista
        >>> cell0_ids = [8, 0, 1, 2, 3, 4, 5, 6, 7]
        >>> cell1_ids = [8, 8, 9, 10, 11, 12, 13, 14, 15]
        >>> cells = np.hstack((cell0_ids, cell1_ids))
        >>> cell_type = np.array(
        ...     [CellType.HEXAHEDRON, CellType.HEXAHEDRON], np.int8
        ... )

        >>> cell1 = np.array(
        ...     [
        ...         [0, 0, 0],
        ...         [1, 0, 0],
        ...         [1, 1, 0],
        ...         [0, 1, 0],
        ...         [0, 0, 1],
        ...         [1, 0, 1],
        ...         [1, 1, 1],
        ...         [0, 1, 1],
        ...     ],
        ...     dtype=np.float32,
        ... )

        >>> cell2 = np.array(
        ...     [
        ...         [0, 0, 2],
        ...         [1, 0, 2],
        ...         [1, 1, 2],
        ...         [0, 1, 2],
        ...         [0, 0, 3],
        ...         [1, 0, 3],
        ...         [1, 1, 3],
        ...         [0, 1, 3],
        ...     ],
        ...     dtype=np.float32,
        ... )

        >>> points = np.vstack((cell1, cell2))

        >>> grid = pyvista.UnstructuredGrid(cells, cell_type, points)

        """
        # convert to arrays upfront
        cells = np.asarray(cells)
        cell_type = np.asarray(cell_type)
        points = np.asarray(points)

        # Convert to vtk arrays
        vtkcells = CellArray(cells, cell_type.size, deep)
        if cell_type.dtype != np.uint8:
            cell_type = cell_type.astype(np.uint8)
        cell_type = _vtk.numpy_to_vtk(cell_type, deep=deep)

        points = pyvista.vtk_points(points, deep, force_float)
        self.SetPoints(points)

        self.SetCells(cell_type, vtkcells)

    def _check_for_consistency(self):
        """Check if size of offsets and celltypes match the number of cells.

        Checks if the number of offsets and celltypes correspond to
        the number of cells.  Called after initialization of the self
        from arrays.
        """
        if self.n_cells != self.celltypes.size:
            raise ValueError(
                f'Number of cell types ({self.celltypes.size}) '
                f'must match the number of cells {self.n_cells})'
            )

        if self.n_cells != self.offset.size - 1:  # pragma: no cover
            raise ValueError(
                f'Size of the offset ({self.offset.size}) '
                f'must be one greater than the number of cells ({self.n_cells})'
            )

    @property
    def cells(self) -> np.ndarray:
        """Return the cell data as a numpy object.

        This is the old style VTK data layout::

           [n0, p0_0, p0_1, ..., p0_n, n1, p1_0, p1_1, ..., p1_n, ...]

        where ``n0`` is the number of points in cell 0, and ``pX_Y`` is the
        Y'th point in cell X.

        For example, a triangle and a line might be represented as::

           [3, 0, 1, 2, 2, 0, 1]

        Where the two individual cells would be ``[3, 0, 1, 2]`` and ``[2, 0, 1]``.

        See Also
        --------
        pyvista.DataSet.get_cell
        pyvista.UnstructuredGrid.cell_connectivity
        pyvista.UnstructuredGrid.offset

        Notes
        -----
        The array returned cannot be modified in place and will raise a
        ``ValueError`` if attempted.

        You can, however, set the cells directly. See the example.

        Examples
        --------
        Return the indices of the first two cells from the example hex
        beam.  Note how the cells have "padding" indicating the number
        of points per cell.

        >>> import pyvista
        >>> from pyvista import examples
        >>> grid = examples.load_hexbeam()
        >>> grid.cells[:18]
        array([ 8,  0,  2,  8,  7, 27, 36, 90, 81,  8,  2,  1,  4,  8, 36, 18, 54,
               90])

        While you cannot change the array inplace, you can overwrite it. For example:

        >>> grid.cells = [8, 0, 1, 2, 3, 4, 5, 6, 7]

        """
        # Flag this array as read only to ensure users do not attempt to write to it.
        array = _vtk.vtk_to_numpy(self.GetCells().GetData())
        array.flags['WRITEABLE'] = False
        return array

    @cells.setter
    def cells(self, cells):
        vtk_idarr = numpy_to_idarr(cells, deep=False, return_ind=False)
        self.GetCells().ImportLegacyFormat(vtk_idarr)

    @property
    def cells_dict(self) -> dict:
        """Return a dictionary that contains all cells mapped from cell types.

        This function returns a :class:`numpy.ndarray` for each cell
        type in an ordered fashion.  Note that this function only
        works with element types of fixed sizes.

        Returns
        -------
        dict
            A dictionary mapping containing all cells of this unstructured grid.
            Structure: vtk_enum_type (int) -> cells (:class:`numpy.ndarray`).

        See Also
        --------
        pyvista.DataSet.get_cell

        Examples
        --------
        Return the cells dictionary of the sample hex beam.  Note how
        there is only one key/value pair as the hex beam example is
        composed of only all hexahedral cells, which is
        ``CellType.HEXAHEDRON``, which evaluates to 12.

        Also note how there is no padding for the cell array.  This
        approach may be more helpful than the ``cells`` property when
        extracting cells.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> hex_beam.cells_dict  # doctest:+SKIP
        {12: array([[ 0,  2,  8,  7, 27, 36, 90, 81],
                [ 2,  1,  4,  8, 36, 18, 54, 90],
                [ 7,  8,  6,  5, 81, 90, 72, 63],
                ...
                [44, 26, 62, 98, 11, 10, 13, 17],
                [89, 98, 80, 71, 16, 17, 15, 14],
                [98, 62, 53, 80, 17, 13, 12, 15]])}
        """
        return get_mixed_cells(self)

    @property
    def cell_connectivity(self) -> np.ndarray:
        """Return a the vtk cell connectivity as a numpy array.

        This is effectively :attr:`UnstructuredGrid.cells` without the
        padding.

        Returns
        -------
        numpy.ndarray
            Connectivity array.

        See Also
        --------
        pyvista.DataSet.get_cell

        Examples
        --------
        Return the cell connectivity for the first two cells.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> hex_beam.cell_connectivity[:16]
        array([ 0,  2,  8,  7, 27, 36, 90, 81,  2,  1,  4,  8, 36, 18, 54, 90])

        """
        carr = self.GetCells()
        return _vtk.vtk_to_numpy(carr.GetConnectivityArray())

    def linear_copy(self, deep=False):
        """Return a copy of the unstructured grid containing only linear cells.

        Converts the following cell types to their linear equivalents.

        - ``QUADRATIC_TETRA      --> TETRA``
        - ``QUADRATIC_PYRAMID    --> PYRAMID``
        - ``QUADRATIC_WEDGE      --> WEDGE``
        - ``QUADRATIC_HEXAHEDRON --> HEXAHEDRON``

        Parameters
        ----------
        deep : bool, default: False
            When ``True``, makes a copy of the points array.
            Cells and cell types are always copied.

        Returns
        -------
        pyvista.UnstructuredGrid
            UnstructuredGrid containing only linear cells when
            ``deep=False``.

        """
        lgrid = self.copy(deep)

        # grab the vtk object
        vtk_cell_type = _vtk.numpy_to_vtk(self.GetCellTypesArray(), deep=True)
        celltype = _vtk.vtk_to_numpy(vtk_cell_type)
        celltype[celltype == CellType.QUADRATIC_TETRA] = CellType.TETRA
        celltype[celltype == CellType.QUADRATIC_PYRAMID] = CellType.PYRAMID
        celltype[celltype == CellType.QUADRATIC_WEDGE] = CellType.WEDGE
        celltype[celltype == CellType.QUADRATIC_HEXAHEDRON] = CellType.HEXAHEDRON

        # track quad mask for later
        quad_quad_mask = celltype == CellType.QUADRATIC_QUAD
        celltype[quad_quad_mask] = CellType.QUAD

        quad_tri_mask = celltype == CellType.QUADRATIC_TRIANGLE
        celltype[quad_tri_mask] = CellType.TRIANGLE

        vtk_offset = self.GetCellLocationsArray()
        cells = _vtk.vtkCellArray()
        cells.DeepCopy(self.GetCells())
        lgrid.SetCells(vtk_cell_type, vtk_offset, cells)

        # fixing bug with display of quad cells
        if np.any(quad_quad_mask):
            quad_offset = lgrid.offset[:-1][quad_quad_mask]
            base_point = lgrid.cell_connectivity[quad_offset]
            lgrid.cell_connectivity[quad_offset + 4] = base_point
            lgrid.cell_connectivity[quad_offset + 5] = base_point
            lgrid.cell_connectivity[quad_offset + 6] = base_point
            lgrid.cell_connectivity[quad_offset + 7] = base_point

        if np.any(quad_tri_mask):
            tri_offset = lgrid.offset[:-1][quad_tri_mask]
            base_point = lgrid.cell_connectivity[tri_offset]
            lgrid.cell_connectivity[tri_offset + 3] = base_point
            lgrid.cell_connectivity[tri_offset + 4] = base_point
            lgrid.cell_connectivity[tri_offset + 5] = base_point

        return lgrid

    @property
    def celltypes(self) -> np.ndarray:
        """Return the cell types array.

        Returns
        -------
        numpy.ndarray
            Array of cell types.

        Notes
        -----
        Here are some of the most popular cell types:

        * ``EMPTY_CELL = 0``
        * ``VERTEX = 1``
        * ``POLY_VERTEX = 2``
        * ``LINE = 3``
        * ``POLY_LINE = 4``
        * ``TRIANGLE = 5``
        * ``TRIANGLE_STRIP = 6``
        * ``POLYGON = 7``
        * ``PIXEL = 8``
        * ``QUAD = 9``
        * ``TETRA = 10``
        * ``VOXEL = 11``
        * ``HEXAHEDRON = 12``
        * ``WEDGE = 13``
        * ``PYRAMID = 14``
        * ``PENTAGONAL_PRISM = 15``
        * ``HEXAGONAL_PRISM = 16``
        * ``QUADRATIC_EDGE = 21``
        * ``QUADRATIC_TRIANGLE = 22``
        * ``QUADRATIC_QUAD = 23``
        * ``QUADRATIC_POLYGON = 36``
        * ``QUADRATIC_TETRA = 24``
        * ``QUADRATIC_HEXAHEDRON = 25``
        * ``QUADRATIC_WEDGE = 26``
        * ``QUADRATIC_PYRAMID = 27``
        * ``BIQUADRATIC_QUAD = 28``
        * ``TRIQUADRATIC_HEXAHEDRON = 29``
        * ``QUADRATIC_LINEAR_QUAD = 30``
        * ``QUADRATIC_LINEAR_WEDGE = 31``
        * ``BIQUADRATIC_QUADRATIC_WEDGE = 32``
        * ``BIQUADRATIC_QUADRATIC_HEXAHEDRON = 33``
        * ``BIQUADRATIC_TRIANGLE = 34``

        See `vtkCellType.h
        <https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html>`_ for all
        cell types.

        Examples
        --------
        This mesh contains only linear hexahedral cells, type
        ``CellType.HEXAHEDRON``, which evaluates to 12.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> hex_beam.celltypes  # doctest:+SKIP
        array([12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
               12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
               12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12],
               dtype=uint8)

        """
        return _vtk.vtk_to_numpy(self.GetCellTypesArray())

    @property
    def offset(self) -> np.ndarray:
        """Return the cell locations array.

        This is the location of the start of each cell in
        :attr:`cell_connectivity`.

        Returns
        -------
        numpy.ndarray
            Array of cell offsets indicating the start of each cell.

        Notes
        -----
        The array returned is immutable and cannot be written to. If you
        need to modify this array, create a copy of it using
        :func:`numpy.copy`.

        Examples
        --------
        Return the cell offset array.  Since this mesh is composed of
        all hexahedral cells, note how each cell starts at 8 greater
        than the prior cell.

        >>> import pyvista
        >>> from pyvista import examples
        >>> hex_beam = pyvista.read(examples.hexbeamfile)
        >>> hex_beam.offset
        array([  0,   8,  16,  24,  32,  40,  48,  56,  64,  72,  80,  88,  96,
               104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200,
               208, 216, 224, 232, 240, 248, 256, 264, 272, 280, 288, 296, 304,
               312, 320])

        """
        carr = self.GetCells()
        # This will be the number of cells + 1.
        array = _vtk.vtk_to_numpy(carr.GetOffsetsArray())
        array.flags['WRITEABLE'] = False
        return array

    def cast_to_explicit_structured_grid(self):
        """Cast to an explicit structured grid.

        Returns
        -------
        pyvista.ExplicitStructuredGrid
            An explicit structured grid.

        Raises
        ------
        TypeError
            If the unstructured grid doesn't have the ``'BLOCK_I'``,
            ``'BLOCK_J'`` and ``'BLOCK_K'`` cells arrays.

        See Also
        --------
        pyvista.ExplicitStructuredGrid.cast_to_unstructured_grid

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.hide_cells(range(80, 120))
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.cast_to_unstructured_grid()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.cast_to_explicit_structured_grid()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        """
        s1 = {'BLOCK_I', 'BLOCK_J', 'BLOCK_K'}
        s2 = self.cell_data.keys()
        if not s1.issubset(s2):
            raise TypeError("'BLOCK_I', 'BLOCK_J' and 'BLOCK_K' cell arrays are required")
        alg = _vtk.vtkUnstructuredGridToExplicitStructuredGrid()
        alg.SetInputData(self)
        alg.SetInputArrayToProcess(0, 0, 0, 1, 'BLOCK_I')
        alg.SetInputArrayToProcess(1, 0, 0, 1, 'BLOCK_J')
        alg.SetInputArrayToProcess(2, 0, 0, 1, 'BLOCK_K')
        alg.Update()
        grid = _get_output(alg)
        grid.cell_data.remove('ConnectivityFlags')  # unrequired
        return grid


class StructuredGrid(_vtk.vtkStructuredGrid, PointGrid, StructuredGridFilters):
    """Dataset used for topologically regular arrays of data.

    Can be initialized in one of the following several ways:

    * Create empty grid.
    * Initialize from a filename.
    * Initialize from a ``vtk.vtkStructuredGrid`` object.
    * Initialize directly from one or more :class:`numpy.ndarray`. See the
      example or the documentation of ``uinput``.

    Parameters
    ----------
    uinput : str, pathlib.Path, vtk.vtkStructuredGrid, numpy.ndarray, optional
        Filename, dataset, or array to initialize the structured grid from. If
        a filename is passed, pyvista will attempt to load it as a
        :class:`StructuredGrid`. If passed a ``vtk.vtkStructuredGrid``, it will
        be wrapped as a deep copy.

        If a :class:`numpy.ndarray` is provided and ``y`` and ``z`` are empty,
        this array will define the points of this :class:`StructuredGrid`.
        Set the dimensions with :attr:`StructuredGrid.dimensions`.

        Otherwise, this parameter will be loaded as the ``x`` points, and ``y``
        and ``z`` points must be set. The shape of this array defines the shape
        of the structured data and the shape should be ``(dimx, dimy,
        dimz)``. Missing trailing dimensions are assumed to be ``1``.

    y : numpy.ndarray, optional
        Coordinates of the points in y direction. If this is passed, ``uinput``
        must be a :class:`numpy.ndarray` and match the shape of ``y``.

    z : numpy.ndarray, optional
        Coordinates of the points in z direction. If this is passed, ``uinput``
        and ``y`` must be a :class:`numpy.ndarray` and match the shape of ``z``.

    deep : optional
        Whether to deep copy a StructuredGrid object.
        Default is ``False``.  Keyword only.

    **kwargs : dict, optional
        Additional keyword arguments passed when reading from a file or loading
        from arrays.

    Examples
    --------
    >>> import pyvista
    >>> import vtk
    >>> import numpy as np

    Create an empty structured grid.

    >>> grid = pyvista.StructuredGrid()

    Initialize from a ``vtk.vtkStructuredGrid`` object

    >>> vtkgrid = vtk.vtkStructuredGrid()
    >>> grid = pyvista.StructuredGrid(vtkgrid)

    Create from NumPy arrays.

    >>> xrng = np.arange(-10, 10, 2, dtype=np.float32)
    >>> yrng = np.arange(-10, 10, 5, dtype=np.float32)
    >>> zrng = np.arange(-10, 10, 1, dtype=np.float32)
    >>> x, y, z = np.meshgrid(xrng, yrng, zrng, indexing='ij')
    >>> grid = pyvista.StructuredGrid(x, y, z)
    >>> grid
    StructuredGrid (...)
      N Cells:      513
      N Points:     800
      X Bounds:     -1.000e+01, 8.000e+00
      Y Bounds:     -1.000e+01, 5.000e+00
      Z Bounds:     -1.000e+01, 9.000e+00
      Dimensions:   10, 4, 20
      N Arrays:     0

    """

    _WRITERS = {'.vtk': _vtk.vtkStructuredGridWriter, '.vts': _vtk.vtkXMLStructuredGridWriter}

    def __init__(self, uinput=None, y=None, z=None, *args, deep=False, **kwargs) -> None:
        """Initialize the structured grid."""
        super().__init__()

        if args:
            raise ValueError("Too many args to create StructuredGrid.")

        if isinstance(uinput, _vtk.vtkStructuredGrid):
            if deep:
                self.deep_copy(uinput)
            else:
                self.shallow_copy(uinput)
        elif isinstance(uinput, (str, pathlib.Path)):
            self._from_file(uinput, **kwargs)
        elif (
            isinstance(uinput, np.ndarray)
            and isinstance(y, np.ndarray)
            and isinstance(z, np.ndarray)
        ):
            self._from_arrays(uinput, y, z, **kwargs)
        elif isinstance(uinput, np.ndarray) and y is None and z is None:
            self.points = uinput  # type: ignore
        elif uinput is None:
            # do nothing, initialize as empty structured grid
            pass
        else:
            raise TypeError(
                "Invalid parameters. Expecting one of the following:\n"
                " - No arguments\n"
                " - Filename as the only argument\n"
                " - StructuredGrid as the only argument\n"
                " - Single `numpy.ndarray` as the only argument"
                " - Three `numpy.ndarray` as the first three arguments"
            )

    def __repr__(self):
        """Return the standard representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the standard str representation."""
        return DataSet.__str__(self)

    def _from_arrays(self, x, y, z, force_float=True):
        """Create VTK structured grid directly from numpy arrays.

        Parameters
        ----------
        x : numpy.ndarray
            Position of the points in x direction.

        y : numpy.ndarray
            Position of the points in y direction.

        z : numpy.ndarray
            Position of the points in z direction.

        force_float : bool, optional
            Casts the datatype to ``float32`` if points datatype is
            non-float.  Default ``True``. Set this to ``False`` to allow
            non-float types, though this may lead to truncation of
            intermediate floats when transforming datasets.

        """
        if not (x.shape == y.shape == z.shape):
            raise ValueError('Input point array shapes must match exactly')

        # make the output points the same precision as the input arrays
        points = np.empty((x.size, 3), x.dtype)
        points[:, 0] = x.ravel('F')
        points[:, 1] = y.ravel('F')
        points[:, 2] = z.ravel('F')

        # ensure that the inputs are 3D
        dim = list(x.shape)
        while len(dim) < 3:
            dim.append(1)

        # Create structured grid
        self.SetDimensions(dim)
        self.SetPoints(pyvista.vtk_points(points, force_float=force_float))

    @property
    def dimensions(self):
        """Return a length 3 tuple of the grid's dimensions.

        Returns
        -------
        tuple
            Grid dimensions.

        Examples
        --------
        >>> import pyvista
        >>> import numpy as np
        >>> xrng = np.arange(-10, 10, 1, dtype=np.float32)
        >>> yrng = np.arange(-10, 10, 2, dtype=np.float32)
        >>> zrng = np.arange(-10, 10, 5, dtype=np.float32)
        >>> x, y, z = np.meshgrid(xrng, yrng, zrng, indexing='ij')
        >>> grid = pyvista.StructuredGrid(x, y, z)
        >>> grid.dimensions
        (20, 10, 4)

        """
        return tuple(self.GetDimensions())

    @dimensions.setter
    def dimensions(self, dims):
        nx, ny, nz = dims[0], dims[1], dims[2]
        self.SetDimensions(nx, ny, nz)
        self.Modified()

    @property
    def x(self):
        """Return the X coordinates of all points.

        Returns
        -------
        numpy.ndarray
            Numpy array of all X coordinates.

        Examples
        --------
        >>> import pyvista
        >>> import numpy as np
        >>> xrng = np.arange(-10, 10, 1, dtype=np.float32)
        >>> yrng = np.arange(-10, 10, 2, dtype=np.float32)
        >>> zrng = np.arange(-10, 10, 5, dtype=np.float32)
        >>> x, y, z = np.meshgrid(xrng, yrng, zrng, indexing='ij')
        >>> grid = pyvista.StructuredGrid(x, y, z)
        >>> grid.x.shape
        (20, 10, 4)

        """
        return self._reshape_point_array(self.points[:, 0])

    @property
    def y(self):
        """Return the Y coordinates of all points."""
        return self._reshape_point_array(self.points[:, 1])

    @property
    def z(self):
        """Return the Z coordinates of all points."""
        return self._reshape_point_array(self.points[:, 2])

    @property
    def points_matrix(self):
        """Points as a 4-D matrix, with x/y/z along the last dimension."""
        return self.points.reshape((*self.dimensions, 3), order='F')

    def _get_attrs(self):
        """Return the representation methods (internal helper)."""
        attrs = PointGrid._get_attrs(self)
        attrs.append(("Dimensions", self.dimensions, "{:d}, {:d}, {:d}"))
        return attrs

    def __getitem__(self, key):
        """Slice subsets of the StructuredGrid, or extract an array field."""
        # legacy behavior which looks for a point or cell array
        if not isinstance(key, tuple):
            return super().__getitem__(key)

        # convert slice to VOI specification - only "basic indexing" is supported
        voi = []
        rate = []
        if len(key) != 3:
            raise RuntimeError('Slices must have exactly 3 dimensions.')
        for i, k in enumerate(key):
            if isinstance(k, collections.abc.Iterable):
                raise RuntimeError('Fancy indexing is not supported.')
            if isinstance(k, numbers.Integral):
                start = stop = k
                step = 1
            elif isinstance(k, slice):
                start = k.start if k.start is not None else 0
                stop = k.stop - 1 if k.stop is not None else self.dimensions[i]
                step = k.step if k.step is not None else 1
            voi.extend((start, stop))
            rate.append(step)

        return self.extract_subset(voi, rate, boundary=False)

    def hide_cells(self, ind, inplace=False):
        """Hide cells without deleting them.

        Hides cells by setting the ghost_cells array to ``HIDDEN_CELL``.

        Parameters
        ----------
        ind : sequence[int]
            List or array of cell indices to be hidden.  The array can
            also be a boolean array of the same size as the number of
            cells.

        inplace : bool, default: False
            Updates mesh in-place.

        Returns
        -------
        pyvista.StructuredGrid
            Structured grid with hidden cells.

        Examples
        --------
        Hide part of the middle of a structured surface.

        >>> import pyvista as pv
        >>> import numpy as np
        >>> x = np.arange(-10, 10, 0.25)
        >>> y = np.arange(-10, 10, 0.25)
        >>> z = 0
        >>> x, y, z = np.meshgrid(x, y, z)
        >>> grid = pv.StructuredGrid(x, y, z)
        >>> grid = grid.hide_cells(range(79 * 30, 79 * 50))
        >>> grid.plot(color=True, show_edges=True)
        """
        if not inplace:
            return self.copy().hide_cells(ind, inplace=True)
        if isinstance(ind, np.ndarray):
            if ind.dtype == np.bool_ and ind.size != self.n_cells:
                raise ValueError(
                    f'Boolean array size must match the number of cells ({self.n_cells})'
                )
        ghost_cells = np.zeros(self.n_cells, np.uint8)
        ghost_cells[ind] = _vtk.vtkDataSetAttributes.HIDDENCELL

        # NOTE: cells cannot be removed from a structured grid, only
        # hidden setting ghost_cells to a value besides
        # vtk.vtkDataSetAttributes.HIDDENCELL will not hide them
        # properly, additionally, calling self.RemoveGhostCells will
        # have no effect

        # add but do not make active
        self.cell_data.set_array(ghost_cells, _vtk.vtkDataSetAttributes.GhostArrayName())
        return self

    def hide_points(self, ind):
        """Hide points without deleting them.

        Hides points by setting the ghost_points array to ``HIDDEN_CELL``.

        Parameters
        ----------
        ind : sequence[int]
            Sequence of point indices to be hidden.  The array
            can also be a boolean array of the same size as the number
            of points.

        Returns
        -------
        pyvista.PointSet
            Point set with hidden points.

        Examples
        --------
        Hide part of the middle of a structured surface.

        >>> import pyvista as pv
        >>> import numpy as np
        >>> x = np.arange(-10, 10, 0.25)
        >>> y = np.arange(-10, 10, 0.25)
        >>> z = 0
        >>> x, y, z = np.meshgrid(x, y, z)
        >>> grid = pv.StructuredGrid(x, y, z)
        >>> grid.hide_points(range(80 * 30, 80 * 50))
        >>> grid.plot(color=True, show_edges=True)
        """
        if isinstance(ind, np.ndarray):
            if ind.dtype == np.bool_ and ind.size != self.n_points:
                raise ValueError(
                    f'Boolean array size must match the number of points ({self.n_points})'
                )
        ghost_points = np.zeros(self.n_points, np.uint8)
        ghost_points[ind] = _vtk.vtkDataSetAttributes.HIDDENPOINT

        # add but do not make active
        self.point_data.set_array(ghost_points, _vtk.vtkDataSetAttributes.GhostArrayName())

    def _reshape_point_array(self, array):
        """Reshape point data to a 3-D matrix."""
        return array.reshape(self.dimensions, order='F')

    def _reshape_cell_array(self, array):
        """Reshape cell data to a 3-D matrix."""
        cell_dims = np.array(self.dimensions) - 1
        cell_dims[cell_dims == 0] = 1
        return array.reshape(cell_dims, order='F')


class ExplicitStructuredGrid(_vtk.vtkExplicitStructuredGrid, PointGrid):
    """Extend the functionality of the ``vtk.vtkExplicitStructuredGrid`` class.

    Can be initialized by the following:

    - Creating an empty grid
    - From a ``vtk.vtkExplicitStructuredGrid`` or ``vtk.vtkUnstructuredGrid`` object
    - From a VTU or VTK file
    - From ``dims`` and ``corners`` arrays

    Parameters
    ----------
    args : vtk.vtkExplicitStructuredGrid, vtk.vtkUnstructuredGrid, str, Sequence
        See examples below.
    deep : bool, default: False
        Whether to deep copy a ``vtk.vtkUnstructuredGrid`` object.

    Examples
    --------
    >>> import numpy as np
    >>> import pyvista as pv
    >>>
    >>> # grid size: ni*nj*nk cells; si, sj, sk steps
    >>> ni, nj, nk = 4, 5, 6
    >>> si, sj, sk = 20, 10, 1
    >>>
    >>> # create raw coordinate grid
    >>> grid_ijk = np.mgrid[
    ...     : (ni + 1) * si : si,
    ...     : (nj + 1) * sj : sj,
    ...     : (nk + 1) * sk : sk,
    ... ]
    >>>
    >>> # repeat array along each Cartesian axis for connectivity
    >>> for axis in range(1, 4):
    ...     grid_ijk = grid_ijk.repeat(2, axis=axis)
    ...
    >>>
    >>> # slice off unnecessarily doubled edge coordinates
    >>> grid_ijk = grid_ijk[:, 1:-1, 1:-1, 1:-1]
    >>>
    >>> # reorder and reshape to VTK order
    >>> corners = grid_ijk.transpose().reshape(-1, 3)
    >>>
    >>> dims = np.array([ni, nj, nk]) + 1
    >>> grid = pv.ExplicitStructuredGrid(dims, corners)
    >>> grid = grid.compute_connectivity()
    >>> grid.plot(show_edges=True)

    """

    _WRITERS = {'.vtu': _vtk.vtkXMLUnstructuredGridWriter, '.vtk': _vtk.vtkUnstructuredGridWriter}

    def __init__(self, *args, deep=False, **kwargs):
        """Initialize the explicit structured grid."""
        super().__init__()
        n = len(args)
        if n > 2:
            raise ValueError("Too many args to create ExplicitStructuredGrid.")
        if n == 1:
            arg0 = args[0]
            if isinstance(arg0, _vtk.vtkExplicitStructuredGrid):
                if deep:
                    self.deep_copy(arg0)
                else:
                    self.shallow_copy(arg0)
            elif isinstance(arg0, _vtk.vtkUnstructuredGrid):
                grid = arg0.cast_to_explicit_structured_grid()
                self.shallow_copy(grid)
            elif isinstance(arg0, (str, pathlib.Path)):
                grid = UnstructuredGrid(arg0)
                grid = grid.cast_to_explicit_structured_grid()
                self.shallow_copy(grid)
        elif n == 2:
            arg0, arg1 = args
            if isinstance(arg0, tuple):
                arg0 = np.asarray(arg0)
            if isinstance(arg1, list):
                arg1 = np.asarray(arg1)
            arg0_is_arr = isinstance(arg0, np.ndarray)
            arg1_is_arr = isinstance(arg1, np.ndarray)
            if all([arg0_is_arr, arg1_is_arr]):
                self._from_arrays(arg0, arg1)

    def __repr__(self):
        """Return the standard representation."""
        return DataSet.__repr__(self)

    def __str__(self):
        """Return the standard ``str`` representation."""
        return DataSet.__str__(self)

    def _from_arrays(self, dims: Sequence, corners: Sequence) -> None:
        """Create a VTK explicit structured grid from NumPy arrays.

        Parameters
        ----------
        dims : sequence[int]
            A sequence of integers with shape (3,) containing the
            topological dimensions of the grid.

        corners : array_like[floats]
            A sequence of floats with shape (number of corners, 3)
            containing the coordinates of the corner points.

        """
        shape0 = np.asanyarray(dims) - 1
        shape1 = 2 * shape0
        ncells = np.prod(shape0)
        cells = 8 * np.ones((ncells, 9), dtype=int)
        points, indices = np.unique(corners, axis=0, return_inverse=True)
        connectivity = np.asarray(
            [[0, 1, 1, 0, 0, 1, 1, 0], [0, 0, 1, 1, 0, 0, 1, 1], [0, 0, 0, 0, 1, 1, 1, 1]]
        )
        for c in range(ncells):
            i, j, k = np.unravel_index(c, shape0, order='F')
            coord = (2 * i + connectivity[0], 2 * j + connectivity[1], 2 * k + connectivity[2])
            cinds = np.ravel_multi_index(coord, shape1, order='F')  # type: ignore
            cells[c, 1:] = indices[cinds]
        cells = cells.flatten()
        points = pyvista.vtk_points(points)
        cells = CellArray(cells, ncells)
        self.SetDimensions(dims)
        self.SetPoints(points)
        self.SetCells(cells)

    def cast_to_unstructured_grid(self) -> 'UnstructuredGrid':
        """Cast to an unstructured grid.

        Returns
        -------
        UnstructuredGrid
            An unstructured grid. VTK adds the ``'BLOCK_I'``,
            ``'BLOCK_J'`` and ``'BLOCK_K'`` cell arrays. These arrays
            are required to restore the explicit structured grid.

        See Also
        --------
        pyvista.DataSetFilters.extract_cells : Extract a subset of a dataset.
        pyvista.UnstructuredGrid.cast_to_explicit_structured_grid : Cast an unstructured grid to an explicit structured grid.

        Notes
        -----
        The ghost cell array is disabled before casting the
        unstructured grid in order to allow the original structure
        and attributes data of the explicit structured grid to be
        restored. If you don't need to restore the explicit
        structured grid later or want to extract an unstructured
        grid from the visible subgrid, use the ``extract_cells``
        filter and the cell indices where the ghost cell array is
        ``0``.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.hide_cells(range(80, 120))
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.cast_to_unstructured_grid()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.cast_to_explicit_structured_grid()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        """
        grid = ExplicitStructuredGrid()
        grid.copy_structure(self)
        alg = _vtk.vtkExplicitStructuredGridToUnstructuredGrid()
        alg.SetInputDataObject(grid)
        alg.Update()
        grid = _get_output(alg)
        grid.cell_data.remove('vtkOriginalCellIds')  # unrequired
        grid.copy_attributes(self)  # copy ghost cell array and other arrays
        return grid

    def save(self, filename, binary=True):
        """Save this VTK object to file.

        Parameters
        ----------
        filename : str
            Output file name. VTU and VTK extensions are supported.

        binary : bool, default: True
            If ``True``, write as binary, else ASCII.

        Notes
        -----
        VTK adds the ``'BLOCK_I'``, ``'BLOCK_J'`` and ``'BLOCK_K'``
        cell arrays. These arrays are required to restore the explicit
        structured grid.

        Examples
        --------
        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()  # doctest:+SKIP
        >>> grid = grid.hide_cells(range(80, 120))  # doctest:+SKIP
        >>> grid.save('grid.vtu')  # doctest:+SKIP

        >>> grid = pv.ExplicitStructuredGrid('grid.vtu')  # doctest:+SKIP
        >>> grid.plot(
        ...     color='w', show_edges=True, show_bounds=True
        ... )  # doctest:+SKIP

        >>> grid.show_cells()  # doctest:+SKIP
        >>> grid.plot(
        ...     color='w', show_edges=True, show_bounds=True
        ... )  # doctest:+SKIP

        """
        grid = self.cast_to_unstructured_grid()
        grid.save(filename, binary)

    def hide_cells(self, ind: Sequence[int], inplace=False) -> 'ExplicitStructuredGrid':
        """Hide specific cells.

        Hides cells by setting the ghost cell array to ``HIDDENCELL``.

        Parameters
        ----------
        ind : sequence[int]
            Cell indices to be hidden. A boolean array of the same
            size as the number of cells also is acceptable.

        inplace : bool, default: False
            This method is applied to this grid if ``True``
            or to a copy otherwise.

        Returns
        -------
        ExplicitStructuredGrid or None
            A deep copy of this grid if ``inplace=False`` with the
            hidden cells, or this grid with the hidden cells if
            otherwise.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid = grid.hide_cells(range(80, 120))
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        """
        ind_arr = np.asanyarray(ind)

        if inplace:
            array = np.zeros(self.n_cells, dtype=np.uint8)
            array[ind_arr] = _vtk.vtkDataSetAttributes.HIDDENCELL
            name = _vtk.vtkDataSetAttributes.GhostArrayName()
            self.cell_data[name] = array
            return self

        grid = self.copy()
        grid.hide_cells(ind, inplace=True)
        return grid

    def show_cells(self, inplace=False) -> 'ExplicitStructuredGrid':
        """Show hidden cells.

        Shows hidden cells by setting the ghost cell array to ``0``
        where ``HIDDENCELL``.

        Parameters
        ----------
        inplace : bool, default: False
            This method is applied to this grid if ``True``
            or to a copy otherwise.

        Returns
        -------
        ExplicitStructuredGrid
            A deep copy of this grid if ``inplace=False`` with the
            hidden cells shown.  Otherwise, this dataset with the
            shown cells.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid = grid.hide_cells(range(80, 120))
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        >>> grid = grid.show_cells()
        >>> grid.plot(color='w', show_edges=True, show_bounds=True)

        """
        if inplace:
            name = _vtk.vtkDataSetAttributes.GhostArrayName()
            if name in self.cell_data.keys():
                array = self.cell_data[name]
                ind = np.argwhere(array == _vtk.vtkDataSetAttributes.HIDDENCELL)
                array[ind] = 0
            return self
        else:
            grid = self.copy()
            grid.show_cells(inplace=True)
            return grid

    def _dimensions(self):
        # This method is required to avoid conflict if a developer extends `ExplicitStructuredGrid`
        # and reimplements `dimensions` to return, for example, the number of cells in the I, J and
        # K directions.
        dims = self.GetExtent()
        dims = np.reshape(dims, (3, 2))
        dims = np.diff(dims, axis=1)
        dims = dims.flatten() + 1
        return int(dims[0]), int(dims[1]), int(dims[2])

    @property
    def dimensions(self) -> Tuple[int, int, int]:
        """Return the topological dimensions of the grid.

        Returns
        -------
        tuple[int, int, int]
            Number of sampling points in the I, J and Z directions respectively.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid.dimensions
        (5, 6, 7)

        """
        return self._dimensions()

    @property
    def visible_bounds(self) -> BoundsLike:
        """Return the bounding box of the visible cells.

        Different from `bounds`, which returns the bounding box of the
        complete grid, this method returns the bounding box of the
        visible cells, where the ghost cell array is not
        ``HIDDENCELL``.

        Returns
        -------
        tuple[float, float, float]
            The limits of the visible grid in the X, Y and Z
            directions respectively.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid = grid.hide_cells(range(80, 120))
        >>> grid.bounds
        (0.0, 80.0, 0.0, 50.0, 0.0, 6.0)

        >>> grid.visible_bounds
        (0.0, 80.0, 0.0, 50.0, 0.0, 4.0)

        """
        name = _vtk.vtkDataSetAttributes.GhostArrayName()
        if name in self.cell_data:
            array = self.cell_data[name]
            grid = self.extract_cells(array == 0)
            return grid.bounds
        else:
            return self.bounds

    def cell_id(self, coords) -> Union[int, np.ndarray, None]:
        """Return the cell ID.

        Parameters
        ----------
        coords : array_like[int]
            Cell structured coordinates.

        Returns
        -------
        int, numpy.ndarray, or None
            Cell IDs. ``None`` if ``coords`` is outside the grid extent.

        See Also
        --------
        pyvista.ExplicitStructuredGrid.cell_coords : Return the cell structured coordinates.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid.cell_id((3, 4, 0))
        19

        >>> coords = [(3, 4, 0), (3, 2, 1), (1, 0, 2), (2, 3, 2)]
        >>> grid.cell_id(coords)
        array([19, 31, 41, 54])

        """
        # `vtk.vtkExplicitStructuredGrid.ComputeCellId` is not used
        # here because this method returns invalid cell IDs when
        # `coords` is outside the grid extent.
        if isinstance(coords, list):
            coords = np.asarray(coords)
        if isinstance(coords, np.ndarray) and coords.ndim == 2:
            ncol = coords.shape[1]
            coords = [coords[:, c] for c in range(ncol)]
            coords = tuple(coords)
        dims = self._dimensions()
        try:
            ind = np.ravel_multi_index(coords, np.array(dims) - 1, order='F')  # type: ignore
        except ValueError:
            return None
        else:
            return ind

    def cell_coords(self, ind):
        """Return the cell structured coordinates.

        Parameters
        ----------
        ind : int | sequence[int]
            Cell IDs.

        Returns
        -------
        tuple(int), numpy.ndarray, or None
            Cell structured coordinates. ``None`` if ``ind`` is
            outside the grid extent.

        See Also
        --------
        pyvista.ExplicitStructuredGrid.cell_id : Return the cell ID.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid.cell_coords(19)
        (3, 4, 0)

        >>> grid.cell_coords((19, 31, 41, 54))
        array([[3, 4, 0],
               [3, 2, 1],
               [1, 0, 2],
               [2, 3, 2]])

        """
        dims = self._dimensions()
        try:
            coords = np.unravel_index(ind, np.array(dims) - 1, order='F')
        except ValueError:
            return None
        else:
            if isinstance(coords[0], np.ndarray):
                coords = np.stack(coords, axis=1)
            return coords

    def neighbors(self, ind, rel='connectivity') -> list:
        """Return the indices of neighboring cells.

        Parameters
        ----------
        ind : int | sequence[int]
            Cell IDs.

        rel : str, default: "connectivity"
            Defines the neighborhood relationship. If
            ``'topological'``, returns the ``(i-1, j, k)``, ``(i+1, j,
            k)``, ``(i, j-1, k)``, ``(i, j+1, k)``, ``(i, j, k-1)``
            and ``(i, j, k+1)`` cells. If ``'connectivity'``
            (default), returns only the topological neighbors
            considering faces connectivity. If ``'geometric'``,
            returns the cells in the ``(i-1, j)``, ``(i+1, j)``,
            ``(i,j-1)`` and ``(i, j+1)`` vertical cell groups whose
            faces intersect.

        Returns
        -------
        list(int)
            Indices of neighboring cells.

        Examples
        --------
        >>> import pyvista as pv
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> cell = grid.extract_cells(31)
        >>> ind = grid.neighbors(31)
        >>> neighbors = grid.extract_cells(ind)
        >>> plotter = pv.Plotter()
        >>> _ = plotter.add_axes()
        >>> _ = plotter.add_mesh(cell, color='r', show_edges=True)
        >>> _ = plotter.add_mesh(neighbors, color='w', show_edges=True)
        >>> plotter.show()

        """

        def connectivity(ind):
            indices = []
            cell_coords = self.cell_coords(ind)
            cell_points = self.get_cell(ind).points
            if cell_points.shape[0] == 8:
                faces = [
                    [(-1, 0, 0), (0, 4, 7, 3), (1, 5, 6, 2)],
                    [(+1, 0, 0), (1, 2, 6, 5), (0, 3, 7, 4)],
                    [(0, -1, 0), (0, 1, 5, 4), (3, 2, 6, 7)],
                    [(0, +1, 0), (3, 7, 6, 2), (0, 4, 5, 1)],
                    [(0, 0, -1), (0, 3, 2, 1), (4, 7, 6, 5)],
                    [(0, 0, +1), (4, 5, 6, 7), (0, 1, 2, 3)],
                ]
                for f in faces:
                    coords = np.sum([cell_coords, f[0]], axis=0)
                    ind = self.cell_id(coords)
                    if ind:
                        points = self.get_cell(ind).points
                        if points.shape[0] == 8:
                            a1 = cell_points[f[1], :]
                            a2 = points[f[2], :]
                            if np.array_equal(a1, a2):
                                indices.append(ind)
            return indices

        def topological(ind):
            indices = []
            cell_coords = self.cell_coords(ind)
            cell_neighbors = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
            for n in cell_neighbors:
                coords = np.sum([cell_coords, n], axis=0)
                ind = self.cell_id(coords)
                if ind:
                    indices.append(ind)
            return indices

        def geometric(ind):
            indices = []
            cell_coords = self.cell_coords(ind)
            cell_points = self.get_cell(ind).points
            if cell_points.shape[0] == 8:
                for k in [-1, 1]:
                    coords = np.sum([cell_coords, (0, 0, k)], axis=0)
                    ind = self.cell_id(coords)
                    if ind:
                        indices.append(ind)
                faces = [
                    [(-1, 0, 0), (0, 4, 3, 7), (1, 5, 2, 6)],
                    [(+1, 0, 0), (2, 6, 1, 5), (3, 7, 0, 4)],
                    [(0, -1, 0), (1, 5, 0, 4), (2, 6, 3, 7)],
                    [(0, +1, 0), (3, 7, 2, 6), (0, 4, 1, 5)],
                ]
                nk = self.dimensions[2]
                for f in faces:
                    cell_z = cell_points[f[1], 2]
                    cell_z = np.abs(cell_z)
                    cell_z = cell_z.reshape((2, 2))
                    cell_zmin = cell_z.min(axis=1)
                    cell_zmax = cell_z.max(axis=1)
                    coords = np.sum([cell_coords, f[0]], axis=0)
                    for k in range(nk):
                        coords[2] = k
                        ind = self.cell_id(coords)
                        if ind:
                            points = self.get_cell(ind).points
                            if points.shape[0] == 8:
                                z = points[f[2], 2]
                                z = np.abs(z)
                                z = z.reshape((2, 2))
                                zmin = z.min(axis=1)
                                zmax = z.max(axis=1)
                                if (
                                    (zmax[0] > cell_zmin[0] and zmin[0] < cell_zmax[0])
                                    or (zmax[1] > cell_zmin[1] and zmin[1] < cell_zmax[1])
                                    or (zmin[0] > cell_zmax[0] and zmax[1] < cell_zmin[1])
                                    or (zmin[1] > cell_zmax[1] and zmax[0] < cell_zmin[0])
                                ):
                                    indices.append(ind)
            return indices

        if isinstance(ind, int):
            ind = [ind]
        rel = eval(rel)
        indices = set()
        for i in ind:
            indices.update(rel(i))
        return sorted(indices)

    def compute_connectivity(self, inplace=False) -> 'ExplicitStructuredGrid':
        """Compute the faces connectivity flags array.

        This method checks the faces connectivity of the cells with
        their topological neighbors.  The result is stored in the
        array of integers ``'ConnectivityFlags'``. Each value in this
        array must be interpreted as a binary number, where the digits
        shows the faces connectivity of a cell with its topological
        neighbors -Z, +Z, -Y, +Y, -X and +X respectively. For example,
        a cell with ``'ConnectivityFlags'`` equal to ``27``
        (``011011``) indicates that this cell is connected by faces
        with their neighbors ``(0, 0, 1)``, ``(0, -1, 0)``,
        ``(-1, 0, 0)`` and ``(1, 0, 0)``.

        Parameters
        ----------
        inplace : bool, default: False
            This method is applied to this grid if ``True``
            or to a copy otherwise.

        Returns
        -------
        ExplicitStructuredGrid
            A deep copy of this grid if ``inplace=False``, or this
            DataSet if otherwise.

        See Also
        --------
        ExplicitStructuredGrid.compute_connections : Compute an array with the number of connected cell faces.

        Examples
        --------
        >>> from pyvista import examples
        >>>
        >>> grid = examples.load_explicit_structured()
        >>> grid = grid.compute_connectivity()
        >>> grid.plot(show_edges=True)

        """
        if inplace:
            self.ComputeFacesConnectivityFlagsArray()
            return self
        else:
            grid = self.copy()
            grid.compute_connectivity(inplace=True)
            return grid

    def compute_connections(self, inplace=False):
        """Compute an array with the number of connected cell faces.

        This method calculates the number of topological cell
        neighbors connected by faces. The results are stored in the
        ``'number_of_connections'`` cell array.

        Parameters
        ----------
        inplace : bool, default: False
            This method is applied to this grid if ``True`` or to a copy
            otherwise.

        Returns
        -------
        ExplicitStructuredGrid
            A deep copy of this grid if ``inplace=False`` or this
            DataSet if otherwise.

        See Also
        --------
        ExplicitStructuredGrid.compute_connectivity : Compute the faces connectivity flags array.

        Examples
        --------
        >>> from pyvista import examples
        >>> grid = examples.load_explicit_structured()
        >>> grid = grid.compute_connections()
        >>> grid.plot(show_edges=True)

        """
        if inplace:
            if 'ConnectivityFlags' in self.cell_data:
                array = self.cell_data['ConnectivityFlags']
            else:
                grid = self.compute_connectivity(inplace=False)
                array = grid.cell_data['ConnectivityFlags']
            array = array.reshape((-1, 1))
            array = array.astype(np.uint8)
            array = np.unpackbits(array, axis=1)
            array = array.sum(axis=1)
            self.cell_data['number_of_connections'] = array
            return self
        else:
            return self.copy().compute_connections(inplace=True)


# pyvista/core/pyvista_ndarray.py

"""Contains pyvista_ndarray a numpy ndarray type used in pyvista."""
from collections.abc import Iterable
from typing import Union

import numpy as np

from pyvista import _vtk
from pyvista.utilities.helpers import FieldAssociation, convert_array


class pyvista_ndarray(np.ndarray):
    """A ndarray which references the owning dataset and the underlying vtkArray.

    This array can be acted upon just like a :class:`numpy.ndarray`.

    Parameters
    ----------
    array : Iterable or vtk.vtkAbstractArray
        Array like.

    dataset : pyvista.DataSet
        Input dataset.

    association : pyvista.utilities.helpers.FieldAssociation
        Field association.

    Examples
    --------
    Return the points of a Sphere as a :class:`pyvista.pyvista_ndarray`.

    >>> import pyvista as pv
    >>> mesh = pv.Sphere()
    >>> mesh.points  # doctest:+SKIP
    pyvista_ndarray([[-5.5511151e-17,  0.0000000e+00, -5.0000000e-01],
                     [ 5.5511151e-17,  0.0000000e+00,  5.0000000e-01],
                     [-5.4059509e-02,  0.0000000e+00, -4.9706897e-01],
                     ...,
                     [-1.5616201e-01, -3.3193260e-02,  4.7382659e-01],
                     [-1.0513641e-01, -2.2347433e-02,  4.8831028e-01],
                     [-5.2878179e-02, -1.1239604e-02,  4.9706897e-01]],
                    dtype=float32)

    """

    def __new__(
        cls,
        array: Union[Iterable, _vtk.vtkAbstractArray],
        dataset=None,
        association=FieldAssociation.NONE,
    ):
        """Allocate the array."""
        if isinstance(array, Iterable):
            obj = np.asarray(array).view(cls)
        elif isinstance(array, _vtk.vtkAbstractArray):
            obj = convert_array(array).view(cls)
            obj.VTKObject = array
        else:
            raise TypeError(
                f'pyvista_ndarray got an invalid type {type(array)}. '
                'Expected an Iterable or vtk.vtkAbstractArray'
            )

        obj.association = association
        obj.dataset = _vtk.vtkWeakReference()
        if isinstance(dataset, _vtk.VTKObjectWrapper):
            obj.dataset.Set(dataset.VTKObject)
        else:
            obj.dataset.Set(dataset)
        return obj

    def __array_finalize__(self, obj):
        """Finalize array (associate with parent metadata)."""
        # this is necessary to ensure that views/slices of pyvista_ndarray
        # objects stay associated with those of their parents.
        #
        # the VTKArray class uses attributes called `DataSet` and `Assocation`
        # to hold this data. I don't know why this class doesn't use the same
        # convention, but here we just map those over to the appropriate
        # attributes of this class
        _vtk.VTKArray.__array_finalize__(self, obj)
        if np.shares_memory(self, obj):
            self.dataset = getattr(obj, 'dataset', None)
            self.association = getattr(obj, 'association', FieldAssociation.NONE)
            self.VTKObject = getattr(obj, 'VTKObject', None)
        else:
            self.dataset = None
            self.association = FieldAssociation.NONE
            self.VTKObject = None

    def __setitem__(self, key: Union[int, np.ndarray], value):
        """Implement [] set operator.

        When the array is changed it triggers "Modified()" which updates
        all upstream objects, including any render windows holding the
        object.
        """
        super().__setitem__(key, value)
        if self.VTKObject is not None:
            self.VTKObject.Modified()

        # the associated dataset should also be marked as modified
        dataset = self.dataset
        if dataset is not None and dataset.Get():
            dataset.Get().Modified()

    def __array_wrap__(self, out_arr, context=None):
        """Return a numpy scalar if array is 0d.

        See https://github.com/numpy/numpy/issues/5819

        """
        if out_arr.ndim:
            return np.ndarray.__array_wrap__(self, out_arr, context)

        # Match numpy's behavior and return a numpy dtype scalar
        return out_arr[()]

    __getattr__ = _vtk.VTKArray.__getattr__


# pyvista/core/utilities/points.py
"""Points related utilities."""
import warnings

import numpy as np

import pyvista
from pyvista.core import _vtk_core as _vtk


def vtk_points(points, deep=True, force_float=False):
    """Convert numpy array or array-like to a ``vtkPoints`` object.

    Parameters
    ----------
    points : numpy.ndarray or sequence
        Points to convert.  Should be 1 or 2 dimensional.  Accepts a
        single point or several points.

    deep : bool, default: True
        Perform a deep copy of the array.  Only applicable if
        ``points`` is a :class:`numpy.ndarray`.

    force_float : bool, default: False
        Casts the datatype to ``float32`` if points datatype is
        non-float.  Set this to ``False`` to allow non-float types,
        though this may lead to truncation of intermediate floats
        when transforming datasets.

    Returns
    -------
    vtk.vtkPoints
        The vtkPoints object.

    Examples
    --------
    >>> import pyvista
    >>> import numpy as np
    >>> points = np.random.random((10, 3))
    >>> vpoints = pyvista.vtk_points(points)
    >>> vpoints  # doctest:+SKIP
    (vtkmodules.vtkCommonCore.vtkPoints)0x7f0c2e26af40

    """
    points = np.asanyarray(points)

    # verify is numeric
    if not np.issubdtype(points.dtype, np.number):
        raise TypeError('Points must be a numeric type')

    if force_float:
        if not np.issubdtype(points.dtype, np.floating):
            warnings.warn(
                'Points is not a float type. This can cause issues when '
                'transforming or applying filters. Casting to '
                '``np.float32``. Disable this by passing '
                '``force_float=False``.'
            )
            points = points.astype(np.float32)

    # check dimensionality
    if points.ndim == 1:
        points = points.reshape(-1, 3)
    elif points.ndim > 2:
        raise ValueError(f'Dimension of ``points`` should be 1 or 2, not {points.ndim}')

    # verify shape
    if points.shape[1] != 3:
        raise ValueError(
            'Points array must contain three values per point. '
            f'Shape is {points.shape} and should be (X, 3)'
        )

    # use the underlying vtk data if present to avoid memory leaks
    if not deep and isinstance(points, pyvista.pyvista_ndarray):
        if points.VTKObject is not None:
            vtk_object = points.VTKObject

            # we can only use the underlying data if `points` is not a slice of
            # the VTK data object
            if vtk_object.GetSize() == points.size:
                vtkpts = _vtk.vtkPoints()
                vtkpts.SetData(points.VTKObject)
                return vtkpts
            else:
                deep = True

    # points must be contiguous
    points = np.require(points, requirements=['C'])
    vtkpts = _vtk.vtkPoints()
    vtk_arr = _vtk.numpy_to_vtk(points, deep=deep)
    vtkpts.SetData(vtk_arr)

    return vtkpts


def line_segments_from_points(points):
    """Generate non-connected line segments from points.

    Assumes points are ordered as line segments and an even number of
    points.

    Parameters
    ----------
    points : array_like[float]
        Points representing line segments. An even number must be
        given as every two vertices represent a single line
        segment. For example, two line segments would be represented
        as ``np.array([[0, 0, 0], [1, 0, 0], [1, 0, 0], [1, 1, 0]])``.

    Returns
    -------
    pyvista.PolyData
        PolyData with lines and cells.

    Examples
    --------
    This example plots two line segments at right angles to each other.

    >>> import pyvista
    >>> import numpy as np
    >>> points = np.array([[0, 0, 0], [1, 0, 0], [1, 0, 0], [1, 1, 0]])
    >>> lines = pyvista.line_segments_from_points(points)
    >>> lines.plot()

    """
    if len(points) % 2 != 0:
        raise ValueError("An even number of points must be given to define each segment.")
    # Assuming ordered points, create array defining line order
    n_points = len(points)
    n_lines = n_points // 2
    lines = np.c_[
        (
            2 * np.ones(n_lines, np.int_),
            np.arange(0, n_points - 1, step=2),
            np.arange(1, n_points + 1, step=2),
        )
    ]
    poly = pyvista.PolyData()
    poly.points = points
    poly.lines = lines
    return poly


def lines_from_points(points, close=False):
    """Make a connected line set given an array of points.

    Parameters
    ----------
    points : array_like[float]
        Points representing the vertices of the connected
        segments. For example, two line segments would be represented
        as ``np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]])``.

    close : bool, default: False
        If ``True``, close the line segments into a loop.

    Returns
    -------
    pyvista.PolyData
        PolyData with lines and cells.

    Examples
    --------
    >>> import numpy as np
    >>> import pyvista
    >>> points = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]])
    >>> poly = pyvista.lines_from_points(points)
    >>> poly.plot(line_width=5)

    """
    poly = pyvista.PolyData()
    poly.points = points
    cells = np.full((len(points) - 1, 3), 2, dtype=np.int_)
    cells[:, 1] = np.arange(0, len(points) - 1, dtype=np.int_)
    cells[:, 2] = np.arange(1, len(points), dtype=np.int_)
    if close:
        cells = np.append(cells, [[2, len(points) - 1, 0]], axis=0)
    poly.lines = cells
    return poly


def fit_plane_to_points(points, return_meta=False):
    """Fit a plane to a set of points using the SVD algorithm.

    Parameters
    ----------
    points : array_like[float]
        Size ``[N x 3]`` sequence of points to fit a plane through.

    return_meta : bool, default: False
        If ``True``, also returns the center and normal used to
        generate the plane.

    Returns
    -------
    pyvista.PolyData
        Plane mesh.

    numpy.ndarray
        Plane center if ``return_meta=True``.

    numpy.ndarray
        Plane normal if ``return_meta=True``.

    Examples
    --------
    Fit a plane to a random point cloud.

    >>> import pyvista
    >>> import numpy as np
    >>> cloud = np.random.random((10, 3))
    >>> cloud[:, 2] *= 0.1
    >>> plane, center, normal = pyvista.fit_plane_to_points(
    ...     cloud, return_meta=True
    ... )

    Plot the fitted plane.

    >>> pl = pyvista.Plotter()
    >>> _ = pl.add_mesh(
    ...     plane, color='tan', style='wireframe', line_width=4
    ... )
    >>> _ = pl.add_points(
    ...     cloud,
    ...     render_points_as_spheres=True,
    ...     color='r',
    ...     point_size=30,
    ... )
    >>> pl.show()

    """
    data = np.array(points)
    center = data.mean(axis=0)
    result = np.linalg.svd(data - center)
    normal = np.cross(result[2][0], result[2][1])
    plane = pyvista.Plane(center=center, direction=normal)
    if return_meta:
        return plane, center, normal
    return plane


def make_tri_mesh(points, faces):
    """Construct a ``pyvista.PolyData`` mesh using points and faces arrays.

    Construct a mesh from an Nx3 array of points and an Mx3 array of
    triangle indices, resulting in a mesh with N vertices and M
    triangles.  This function does not require the standard VTK
    "padding" column and simplifies mesh creation.

    Parameters
    ----------
    points : np.ndarray
        Array of points with shape ``(N, 3)`` storing the vertices of the
        triangle mesh.

    faces : np.ndarray
        Array of indices with shape ``(M, 3)`` containing the triangle
        indices.

    Returns
    -------
    pyvista.PolyData
        PolyData instance containing the triangle mesh.

    Examples
    --------
    This example discretizes the unit square into a triangle mesh with
    nine vertices and eight faces.

    >>> import numpy as np
    >>> import pyvista
    >>> points = np.array(
    ...     [
    ...         [0, 0, 0],
    ...         [0.5, 0, 0],
    ...         [1, 0, 0],
    ...         [0, 0.5, 0],
    ...         [0.5, 0.5, 0],
    ...         [1, 0.5, 0],
    ...         [0, 1, 0],
    ...         [0.5, 1, 0],
    ...         [1, 1, 0],
    ...     ]
    ... )
    >>> faces = np.array(
    ...     [
    ...         [0, 1, 4],
    ...         [4, 7, 6],
    ...         [2, 5, 4],
    ...         [4, 5, 8],
    ...         [0, 4, 3],
    ...         [3, 4, 6],
    ...         [1, 2, 4],
    ...         [4, 8, 7],
    ...     ]
    ... )
    >>> tri_mesh = pyvista.make_tri_mesh(points, faces)
    >>> tri_mesh.plot(show_edges=True, line_width=5)

    """
    if points.shape[1] != 3:
        raise ValueError("Points array should have shape (N, 3).")
    if faces.ndim != 2 or faces.shape[1] != 3:
        raise ValueError("Face array should have shape (M, 3).")
    cells = np.empty((faces.shape[0], 4), dtype=faces.dtype)
    cells[:, 0] = 3
    cells[:, 1:] = faces
    return pyvista.PolyData(points, cells)


def vector_poly_data(orig, vec):
    """Create a pyvista.PolyData object composed of vectors.

    Parameters
    ----------
    orig : array_like[float]
        Array of vector origins.

    vec : array_like[float]
        Array of vectors.

    Returns
    -------
    pyvista.PolyData
        Mesh containing the ``orig`` points along with the
        ``'vectors'`` and ``'mag'`` point arrays representing the
        vectors and magnitude of the vectors at each point.

    Examples
    --------
    Create basic vector field.  This is a point cloud where each point
    has a vector and magnitude attached to it.

    >>> import pyvista
    >>> import numpy as np
    >>> x, y = np.meshgrid(np.linspace(-5, 5, 10), np.linspace(-5, 5, 10))
    >>> points = np.vstack((x.ravel(), y.ravel(), np.zeros(x.size))).T
    >>> u = x / np.sqrt(x**2 + y**2)
    >>> v = y / np.sqrt(x**2 + y**2)
    >>> vectors = np.vstack(
    ...     (u.ravel() ** 3, v.ravel() ** 3, np.zeros(u.size))
    ... ).T
    >>> pdata = pyvista.vector_poly_data(points, vectors)
    >>> pdata.point_data.keys()
    ['vectors', 'mag']

    Convert these to arrows and plot it.

    >>> pdata.glyph(orient='vectors', scale='mag').plot()

    """
    # shape, dimension checking
    if not isinstance(orig, np.ndarray):
        orig = np.asarray(orig)

    if not isinstance(vec, np.ndarray):
        vec = np.asarray(vec)

    if orig.ndim != 2:
        orig = orig.reshape((-1, 3))
    elif orig.shape[1] != 3:
        raise ValueError('orig array must be 3D')

    if vec.ndim != 2:
        vec = vec.reshape((-1, 3))
    elif vec.shape[1] != 3:
        raise ValueError('vec array must be 3D')

    # Create vtk points and cells objects
    vpts = _vtk.vtkPoints()
    vpts.SetData(_vtk.numpy_to_vtk(np.ascontiguousarray(orig), deep=True))

    npts = orig.shape[0]
    cells = np.empty((npts, 2), dtype=pyvista.ID_TYPE)
    cells[:, 0] = 1
    cells[:, 1] = np.arange(npts, dtype=pyvista.ID_TYPE)
    vcells = pyvista.core.cell.CellArray(cells, npts)

    # Create vtkPolyData object
    pdata = _vtk.vtkPolyData()
    pdata.SetPoints(vpts)
    pdata.SetVerts(vcells)

    # Add vectors to polydata
    name = 'vectors'
    vtkfloat = _vtk.numpy_to_vtk(np.ascontiguousarray(vec), deep=True)
    vtkfloat.SetName(name)
    pdata.GetPointData().AddArray(vtkfloat)
    pdata.GetPointData().SetActiveVectors(name)

    # Add magnitude of vectors to polydata
    name = 'mag'
    scalars = (vec * vec).sum(1) ** 0.5
    vtkfloat = _vtk.numpy_to_vtk(np.ascontiguousarray(scalars), deep=True)
    vtkfloat.SetName(name)
    pdata.GetPointData().AddArray(vtkfloat)
    pdata.GetPointData().SetActiveScalars(name)

    return pyvista.PolyData(pdata)
