Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/plotting/widgets.py
T
2026-02-12 23:27:48 +08:00

3124 lines
114 KiB
Python

"""Module dedicated to widgets."""
from __future__ import annotations
from itertools import product
import pathlib
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.arrays import get_array
from pyvista.core.utilities.arrays import get_array_association
from pyvista.core.utilities.geometric_objects import NORMALS
from pyvista.core.utilities.helpers import generate_plane
from pyvista.core.utilities.misc import abstract_class
from pyvista.core.utilities.misc import assert_empty_kwargs
from pyvista.core.utilities.misc import try_callback
from . import _vtk
from .affine_widget import AffineWidget3D
from .colors import Color
from .opts import PickerType
from .utilities.algorithms import add_ids_algorithm
from .utilities.algorithms import algorithm_to_mesh_handler
from .utilities.algorithms import crinkle_algorithm
from .utilities.algorithms import outline_algorithm
from .utilities.algorithms import pointset_to_polydata_algorithm
from .utilities.algorithms import set_algorithm_input
if TYPE_CHECKING:
from pyvista.core._typing_core import InteractionEventType
from pyvista.core._typing_core import VectorLike
def _parse_interaction_event(interaction_event: InteractionEventType):
"""Parse the interaction event.
Parameters
----------
interaction_event : InteractionEventType
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 not isinstance(interaction_event, (_vtk.vtkCommand.EventIds, str)):
msg = ( # type: ignore[unreachable]
'Expected type for `interaction_event` is either a str '
'or an instance of `vtk.vtkCommand.EventIds`.'
f' ({type(interaction_event)}) was given.'
)
raise TypeError(msg)
if isinstance(interaction_event, _vtk.vtkCommand.EventIds):
return interaction_event
event_map = {
'start': _vtk.vtkCommand.StartInteractionEvent,
'end': _vtk.vtkCommand.EndInteractionEvent,
'always': _vtk.vtkCommand.InteractionEvent,
}
if interaction_event not in event_map:
expected = ', '.join(f'`{e}`' for e in event_map)
msg = (
f'Expected value for `interaction_event` is {expected}. {interaction_event} was given.'
)
raise ValueError(msg)
return event_map[interaction_event]
@abstract_class
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 = []
self.radio_button_widget_dict = {}
self.radio_button_title_dict = {}
self.distance_widgets = []
self.logo_widgets = []
self.camera3d_widgets = []
@_deprecate_positional_args(allowed=['callback'])
def add_box_widget( # noqa: PLR0917
self,
callback,
bounds=None,
factor=1.25,
rotation_enabled: bool = True, # noqa: FBT001, FBT002
color=None,
use_planes: bool = False, # noqa: FBT001, FBT002
outline_translation: bool = True, # noqa: FBT001, FBT002
pass_widget: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = '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 :vtk:`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.plotting.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 : InteractionEventType, 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 box that is used to resize and relocate a sphere.
>>> import pyvista as pv
>>> import numpy as np
>>> plotter = pv.Plotter()
>>> def simulate(widget):
... bounds = widget.bounds
... new_center = np.array(
... [
... (bounds[0] + bounds[1]) / 2,
... (bounds[2] + bounds[3]) / 2,
... (bounds[4] + bounds[5]) / 2,
... ]
... )
... new_radius = (
... min(
... (bounds[1] - bounds[0]) / 2,
... (bounds[3] - bounds[2]) / 2,
... (bounds[5] - bounds[4]) / 2,
... )
... - 0.3
... )
... sphere = pv.Sphere(radius=new_radius, center=new_center)
... _ = plotter.add_mesh(sphere, name='Sphere')
>>> _ = plotter.add_box_widget(callback=simulate)
>>> plotter.show()
"""
if bounds is None:
bounds = self.bounds # type: ignore[attr-defined]
def _the_callback(box_widget, _event):
the_box = pyvista.PolyData()
box_widget.GetPolyData(the_box)
planes = _vtk.vtkPlanes()
box_widget.GetPlanes(planes)
if callable(callback):
args = [planes] if use_planes else [the_box] # type: ignore[list-item]
if pass_widget:
args.append(box_widget)
try_callback(callback, *args)
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) # type: ignore[attr-defined]
box_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
box_widget.SetPlaceFactor(factor)
box_widget.SetRotationEnabled(rotation_enabled)
box_widget.SetTranslationEnabled(outline_translation)
box_widget.PlaceWidget(bounds)
box_widget.On()
box_widget.AddObserver(
_parse_interaction_event(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."""
for box_widget in self.box_widgets:
box_widget.Off()
self.box_widgets.clear()
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_clip_box( # noqa: PLR0917
self,
mesh,
invert: bool = False, # noqa: FBT001, FBT002
rotation_enabled: bool = True, # noqa: FBT001, FBT002
widget_color=None,
outline_translation: bool = True, # noqa: FBT001, FBT002
merge_points: bool = True, # noqa: FBT001, FBT002
crinkle: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = '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 : DataSet | :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 : InteractionEventType, 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:`pyvista.Plotter.add_mesh` to control how the mesh is
displayed.
Returns
-------
:vtk:`vtkActor`
VTK actor of the mesh.
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`.
"""
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'))
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) # type: ignore[attr-defined]
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) # type: ignore[attr-defined]
return self.add_mesh(clipper.GetOutputPort(port), reset_camera=False, **kwargs) # type: ignore[attr-defined]
@_deprecate_positional_args(allowed=['callback'])
def add_plane_widget( # noqa: PLR0917
self,
callback,
normal='x',
origin=None,
bounds=None,
factor=1.25,
color=None,
assign_to_axis=None,
tubing: bool = False, # noqa: FBT001, FBT002
outline_translation: bool = False, # noqa: FBT001, FBT002
origin_translation: bool = True, # noqa: FBT001, FBT002
implicit: bool = True, # noqa: FBT001, FBT002
pass_widget: bool = False, # noqa: FBT001, FBT002
test_callback: bool = True, # noqa: FBT001, FBT002
normal_rotation: bool = True, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
outline_opacity=None,
):
"""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 widget, 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 :vtk:`vtkImplicitPlaneWidget` is used and
when ``False``, a :vtk:`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 : InteractionEventType, 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`.
outline_opacity : bool or float, optional
Set the visible of outline. Only valid when using
an implicit plane. Either a bool or float.
.. versionadded:: 0.44.0
Returns
-------
:vtk:`vtkImplicitPlaneWidget` | :vtk:`vtkPlaneWidget`
Plane widget.
Examples
--------
Shows an interactive plane moving along the x-axis in the random-hill example,
which is used to mark the max altitude at a particular distance x.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.load_random_hills()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh)
>>> def callback(normal, origin):
... slc = mesh.slice(normal=normal, origin=origin)
... origin = list(origin)
... origin[2] = slc.bounds.z_max
... peak_plane = pv.Plane(
... center=origin,
... direction=[0, 0, 1],
... i_size=20,
... j_size=20,
... )
... _ = pl.add_mesh(peak_plane, name='Peak', color='red', opacity=0.4)
>>> _ = pl.add_plane_widget(callback, normal_rotation=False)
>>> pl.show()
"""
if origin is None:
origin = self.center # type: ignore[attr-defined]
if bounds is None:
bounds = self.bounds # type: ignore[attr-defined]
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):
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)
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) # noqa: ARG005
_stop_interact = lambda plane_widget, event: plane_widget.SetDrawPlane(False) # noqa: ARG005
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)
if outline_opacity is not None:
plane_widget.GetOutlineProperty().SetOpacity(float(outline_opacity))
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() # type: ignore[assignment]
plane_widget.SetHandleSize(0.01)
# Position of the widget
plane_widget.SetInputData(source.GetOutput())
plane_widget.SetRepresentationToOutline() # type: ignore[attr-defined]
plane_widget.SetPlaceFactor(factor)
plane_widget.PlaceWidget(bounds)
plane_widget.SetCenter(origin) # type: ignore[attr-defined] # Necessary
plane_widget.GetPlaneProperty().SetColor(color.float_rgb) # self.C_LOT[fn])
plane_widget.GetHandleProperty().SetColor(color.float_rgb) # type: ignore[attr-defined]
if not normal_rotation:
plane_widget.GetHandleProperty().SetOpacity(0) # type: ignore[attr-defined]
plane_widget.GetPlaneProperty().SetOpacity(0.5)
plane_widget.SetInteractor(self.iren.interactor) # type: ignore[attr-defined]
plane_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
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']) # type: ignore[arg-type]
elif assign_to_axis in [1, 'y', 'Y']:
plane_widget.NormalToYAxisOn()
plane_widget.SetNormal(NORMALS['y']) # type: ignore[arg-type]
elif assign_to_axis in [2, 'z', 'Z']:
plane_widget.NormalToZAxisOn()
plane_widget.SetNormal(NORMALS['z']) # type: ignore[arg-type]
else:
msg = 'assign_to_axis not understood'
raise RuntimeError(msg)
else:
plane_widget.SetNormal(normal)
plane_widget.Modified()
plane_widget.UpdatePlacement()
plane_widget.On()
plane_widget.AddObserver(
_parse_interaction_event(interaction_event),
_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."""
for plane_widget in self.plane_widgets:
plane_widget.Off()
self.plane_widgets.clear()
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_clip_plane( # noqa: PLR0917
self,
mesh,
normal='x',
invert: bool = False, # noqa: FBT001, FBT002
widget_color=None,
value=0.0,
assign_to_axis=None,
tubing: bool = False, # noqa: FBT001, FBT002
origin_translation: bool = True, # noqa: FBT001, FBT002
outline_translation: bool = False, # noqa: FBT001, FBT002
implicit: bool = True, # noqa: FBT001, FBT002
normal_rotation: bool = True, # noqa: FBT001, FBT002
crinkle: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
origin=None,
outline_opacity=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 : 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 widget, 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 :vtk:`vtkImplicitPlaneWidget` is used and
when ``False``, a :vtk:`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 : InteractionEventType, 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.
outline_opacity : bool or float, optional
Set the visible of outline. Only valid when using
an implicit plane. Either a bool or float.
.. versionadded:: 0.44.0
**kwargs : dict, optional
All additional keyword arguments are passed to
:func:`pyvista.Plotter.add_mesh` to control how the mesh is
displayed.
Returns
-------
:vtk:`vtkActor`
VTK actor of the mesh.
Examples
--------
Shows an interactive plane used to clip the mesh and store it.
>>> import pyvista as pv
>>> from pyvista import examples
>>> vol = examples.load_airplane()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh_clip_plane(vol, normal=[0, -1, 0])
>>> pl.show(cpos=[-2.1, 0.6, 1.5])
>>> pl.plane_clipped_meshes # doctest:+SKIP
For a full example see :ref:`plane_widget_example`.
"""
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'))
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) # type: ignore[attr-defined]
if isinstance(mesh, _vtk.vtkPolyData):
clipper = _vtk.vtkClipPolyData()
# elif isinstance(mesh, vtk.vtkImageData):
# clipper = vtk.vtkClipVolume()
# clipper.SetMixed3DCellGeneration(True)
else:
clipper = _vtk.vtkTableBasedClipDataSet() # type: ignore[assignment]
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,
outline_opacity=outline_opacity,
)
if crinkle:
return self.add_mesh(crinkler, **kwargs) # type: ignore[attr-defined]
return self.add_mesh(clipper, **kwargs) # type: ignore[attr-defined]
@_deprecate_positional_args(allowed=['volume'])
def add_volume_clip_plane( # noqa: PLR0917
self,
volume,
normal='x',
invert: bool = False, # noqa: ARG002, FBT001, FBT002
widget_color=None,
value=0.0, # noqa: ARG002
assign_to_axis=None,
tubing: bool = False, # noqa: FBT001, FBT002
origin_translation: bool = True, # noqa: FBT001, FBT002
outline_translation: bool = False, # noqa: FBT001, FBT002
implicit: bool = True, # noqa: FBT001, FBT002
normal_rotation: bool = True, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
origin=None,
outline_opacity=None,
**kwargs,
):
"""Clip a volume using a plane widget.
Parameters
----------
volume : pyvista.plotting.volume.Volume or pyvista.ImageData or pyvista.RectilinearGrid
New dataset of type :class:`pyvista.ImageData` or
:class:`pyvista.RectilinearGrid`, or the return value from
:class:`pyvista.plotting.volume.Volume` from :func:`pyvista.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 widget, 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 :vtk:`vtkImplicitPlaneWidget` is used and
when ``False``, a :vtk:`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.
outline_opacity : bool or float, optional
Set the visible of outline. Only valid when using
an implicit plane. Either a bool or float.
.. versionadded:: 0.44.0
**kwargs : dict, optional
All additional keyword arguments are passed to
:func:`pyvista.Plotter.add_volume` to control how the volume is
displayed. Only applicable if ``volume`` is either a
:class:`pyvista.ImageData` and :class:`pyvista.RectilinearGrid`.
Returns
-------
:vtk:`vtkPlaneWidget` | :vtk:`vtkImplicitPlaneWidget`
The VTK plane widget depending on the value of ``implicit``.
See Also
--------
:ref:`clip_volume_widget_example`
"""
if isinstance(volume, (pyvista.ImageData, pyvista.RectilinearGrid)):
volume = self.add_volume(volume, **kwargs) # type: ignore[attr-defined]
elif not isinstance(volume, pyvista.plotting.volume.Volume):
msg = (
'The `volume` parameter type must be either pyvista.ImageData, '
'pyvista.RectilinearGrid, or a pyvista.plotting.volume.Volume '
'from `Plotter.add_volume`.'
)
raise TypeError(msg)
else:
assert_empty_kwargs(**kwargs)
plane = _vtk.vtkPlane()
def callback(normal, origin): # numpydoc ignore=PR01
"""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,
outline_opacity=outline_opacity,
)
widget.GetPlane(plane)
volume.mapper.AddClippingPlane(plane)
self.plane_widgets.append(widget)
return widget
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_slice( # noqa: PLR0917
self,
mesh,
normal='x',
generate_triangles: bool = False, # noqa: FBT001, FBT002
widget_color=None,
assign_to_axis=None,
tubing: bool = False, # noqa: FBT001, FBT002
origin_translation: bool = True, # noqa: FBT001, FBT002
outline_translation: bool = False, # noqa: FBT001, FBT002
implicit: bool = True, # noqa: FBT001, FBT002
normal_rotation: bool = True, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
origin=None,
outline_opacity=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 : DataSet | :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 widget, 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 :vtk:`vtkImplicitPlaneWidget` is used and when
``False``, a :vtk:`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 : InteractionEventType, optional
The VTK interaction event to use for triggering the
callback. Accepts either the strings ``'start'``, ``'end'``,
``'always'`` or a :vtk:`vtkCommand.EventIds`.
origin : tuple(float), optional
The starting coordinate of the center of the plane.
outline_opacity : bool or float, optional
Set the visible of outline. Only valid when using
an implicit plane. Either a bool or float.
.. versionadded:: 0.44.0
**kwargs : dict, optional
All additional keyword arguments are passed to
:func:`pyvista.Plotter.add_mesh` to control how the mesh is
displayed.
Returns
-------
:vtk:`vtkActor`
VTK actor of the mesh.
Examples
--------
Shows an interactive plane used specifically for slicing.
>>> import pyvista as pv
>>> from pyvista import examples
>>> pl = pv.Plotter()
>>> mesh = examples.load_channels()
>>> _ = pl.add_mesh(mesh.outline())
>>> _ = pl.add_mesh_slice(mesh, normal=[1, 0, 0.3])
>>> pl.show()
For a full example see :ref:`plane_widget_example`.
"""
mesh, algo = algorithm_to_mesh_handler(mesh)
name = kwargs.get('name', mesh.memory_address)
rng = mesh.get_data_range(kwargs.get('scalars'))
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) # type: ignore[attr-defined]
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=_parse_interaction_event(interaction_event),
outline_opacity=outline_opacity,
)
return self.add_mesh(alg, **kwargs) # type: ignore[attr-defined]
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_slice_orthogonal( # noqa: PLR0917
self,
mesh,
generate_triangles: bool = False, # noqa: FBT001, FBT002
widget_color=None,
tubing: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
**kwargs,
):
"""Slice a mesh with three interactive planes.
Adds three interactive plane slicing widgets for orthogonal slicing
along each cartesian axis.
Parameters
----------
mesh : 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 widget, this controls whether or not
tubing is shown around the plane's boundaries.
interaction_event : InteractionEventType, optional
The VTK interaction event to use for triggering the
callback. Accepts either the strings ``'start'``, ``'end'``,
``'always'`` or a :vtk:`vtkCommand.EventIds`.
**kwargs : dict, optional
All additional keyword arguments are passed to
:func:`pyvista.Plotter.add_mesh` to control how the mesh is
displayed.
Returns
-------
list
List of :vtk:`vtkActor`.
Examples
--------
Shows an interactive plane sliced along each cartesian axis of the mesh.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> mesh = pv.Wavelet()
>>> _ = pl.add_mesh(mesh.outline())
>>> _ = pl.add_mesh_slice_orthogonal(mesh)
>>> pl.show()
"""
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=_parse_interaction_event(interaction_event),
**axkwargs,
)
actors.append(a)
return actors
@_deprecate_positional_args(allowed=['callback'])
def add_line_widget( # noqa: PLR0917
self,
callback,
bounds=None,
factor=1.25,
resolution=100,
color=None,
use_vertices: bool = False, # noqa: FBT001, FBT002
pass_widget: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
):
"""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 : InteractionEventType, 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:`vtkLineWidget`
Created line widget.
Examples
--------
Shows an interactive line widget to move the sliced object
like in `add_mesh_slice` function.
>>> import pyvista as pv
>>> from pyvista import examples
>>> import numpy as np
>>> model = examples.load_channels()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(model, opacity=0.4)
>>> def move_center(pointa, pointb):
... center = (np.array(pointa) + np.array(pointb)) / 2
... normal = np.array(pointa) - np.array(pointb)
... single_slc = model.slice(normal=normal, origin=center)
...
... _ = pl.add_mesh(single_slc, name='slc')
>>> _ = pl.add_line_widget(callback=move_center, use_vertices=True)
>>> pl.show()
"""
if bounds is None:
bounds = self.bounds # type: ignore[attr-defined]
color = Color(color, default_color=pyvista.global_theme.font.color)
def _the_callback(widget, _event):
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) # type: ignore[attr-defined]
line_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
line_widget.SetPlaceFactor(factor)
line_widget.PlaceWidget(bounds)
line_widget.SetResolution(resolution)
line_widget.Modified()
line_widget.On()
line_widget.AddObserver(
_parse_interaction_event(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."""
for line_widget in self.line_widgets:
line_widget.Off()
self.line_widgets.clear()
@_deprecate_positional_args(allowed=['callback', 'data'])
def add_text_slider_widget( # noqa: PLR0917
self,
callback,
data,
value=None,
pointa=(0.4, 0.9),
pointb=(0.9, 0.9),
color=None,
interaction_event: InteractionEventType = 'end',
style=None,
):
"""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.plotting.themes._Font.color>`.
interaction_event : InteractionEventType, 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``.
Returns
-------
:vtk:`vtkSliderWidget`
The VTK slider widget configured to display text.
"""
if not isinstance(data, list):
msg = (
f'The `data` parameter must be a list but {type(data).__name__} was passed instead'
)
raise TypeError(msg)
n_states = len(data)
if n_states == 0:
msg = 'The input list of values is empty'
raise ValueError(msg)
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])
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
@_deprecate_positional_args(allowed=['callback', 'rng'])
def add_slider_widget( # noqa: PLR0917
self,
callback,
rng,
value=None,
title=None,
pointa=(0.4, 0.9),
pointb=(0.9, 0.9),
color=None,
pass_widget: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
style=None,
title_height=0.03,
title_opacity=1.0,
title_color=None,
fmt=None,
slider_width=None,
tube_width=None,
):
"""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.plotting.themes._Font.color>`.
pass_widget : bool, optional
If ``True``, the widget will be passed as the last
argument of the callback.
interaction_event : InteractionEventType, 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.
Returns
-------
:vtk:`vtkSliderWidget`
Slider widget.
See Also
--------
:ref:`multi_slider_widget_example`
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()
"""
msg = 'Cannot add a widget to a closed plotter.'
iren = self._get_iren_not_none(msg) # type: ignore[attr-defined]
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()) # type: ignore[attr-defined]
pointb = normalize(pointb, self.renderer.GetViewport()) # type: ignore[attr-defined]
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):
msg = f'Expected type for ``style`` is str but {type(style).__name__} was given.'
raise TypeError(msg)
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)
slider_widget = _vtk.vtkSliderWidget()
slider_widget.SetInteractor(iren.interactor)
slider_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
slider_widget.SetRepresentation(slider_rep)
slider_widget.GetRepresentation().SetTitleHeight(title_height) # type: ignore[attr-defined]
slider_widget.GetRepresentation().GetTitleProperty().SetOpacity(title_opacity) # type: ignore[attr-defined]
slider_widget.GetRepresentation().GetTitleProperty().SetColor(title_color.float_rgb) # type: ignore[attr-defined]
if fmt is not None:
slider_widget.GetRepresentation().SetLabelFormat(fmt) # type: ignore[attr-defined]
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."""
for slider_widget in self.slider_widgets:
slider_widget.Off()
self.slider_widgets.clear()
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_threshold( # noqa: PLR0917
self,
mesh,
scalars=None,
invert: bool = False, # noqa: FBT001, FBT002
widget_color=None,
preference='cell',
title=None,
pointa=(0.4, 0.9),
pointb=(0.9, 0.9),
continuous: bool = False, # noqa: FBT001, FBT002
all_scalars: bool = False, # noqa: FBT001, FBT002
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 : 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 pyvista.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):
msg = 'MultiBlock datasets are not supported for threshold widget.'
raise TypeError(msg)
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:
msg = 'No arrays present to threshold.'
raise ValueError(msg)
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) # type: ignore[attr-defined]
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=value, method=method, invert=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) # type: ignore[attr-defined]
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_isovalue( # noqa: PLR0917
self,
mesh,
scalars=None,
compute_normals: bool = False, # noqa: FBT001, FBT002
compute_gradients: bool = False, # noqa: FBT001, FBT002
compute_scalars: bool = True, # noqa: FBT001, FBT002
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 : 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:`pyvista.Plotter.add_mesh` to control how the mesh is
displayed.
Returns
-------
:vtk:`vtkActor`
VTK actor of the mesh.
Examples
--------
Shows an interactive slider controlling the altitude of the contours.
>>> import pyvista as pv
>>> from pyvista import examples
>>> pl = pv.Plotter()
>>> mesh = examples.load_random_hills()
>>> _ = pl.add_mesh(mesh, opacity=0.4)
>>> _ = pl.add_mesh_isovalue(mesh)
>>> pl.show()
"""
mesh, algo = algorithm_to_mesh_handler(mesh)
if isinstance(mesh, pyvista.PointSet):
msg = 'PointSets are 0-dimensional and thus cannot produce contours.'
raise TypeError(msg)
if isinstance(mesh, pyvista.MultiBlock):
msg = 'MultiBlock datasets are not supported for this widget.'
raise TypeError(msg)
name = kwargs.get('name', mesh.memory_address)
# set the array to contour on
if mesh.n_arrays < 1:
msg = 'Input dataset for the contour filter must have data arrays.'
raise ValueError(msg)
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:
msg = (
f'Contour filter only works on Point data. Array ({scalars}) is in the Cell data.'
)
raise TypeError(msg)
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) # type: ignore[attr-defined]
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) # type: ignore[attr-defined]
@_deprecate_positional_args(allowed=['callback'])
def add_spline_widget( # noqa: PLR0917
self,
callback,
bounds=None,
factor=1.25,
n_handles=5,
resolution=25,
color='yellow',
show_ribbon: bool = False, # noqa: FBT001, FBT002
ribbon_color='pink',
ribbon_opacity=0.5,
pass_widget: bool = False, # noqa: FBT001, FBT002
closed: bool = False, # noqa: FBT001, FBT002
initial_points=None,
interaction_event: InteractionEventType = 'end',
):
"""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 : InteractionEventType, 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:`vtkSplineWidget`
The newly created spline widget.
See Also
--------
:ref:`spline_widget_example`
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:
msg = '`initial_points` must be length `n_handles`.'
raise ValueError(msg)
color = Color(color, default_color=pyvista.global_theme.color)
if bounds is None:
bounds = self.bounds # type: ignore[attr-defined]
ribbon = pyvista.PolyData()
def _the_callback(widget, _event):
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)
spline_widget = _vtk.vtkSplineWidget()
spline_widget.GetLineProperty().SetColor(color.float_rgb)
spline_widget.SetNumberOfHandles(n_handles)
spline_widget.SetInteractor(self.iren.interactor) # type: ignore[attr-defined]
spline_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
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(
_parse_interaction_event(interaction_event),
_the_callback,
)
_the_callback(spline_widget, None)
if show_ribbon:
self.add_mesh(ribbon, color=ribbon_color, opacity=ribbon_opacity) # type: ignore[attr-defined]
self.spline_widgets.append(spline_widget)
return spline_widget
def clear_spline_widgets(self):
"""Remove all of the spline widgets."""
for spline_widget in self.spline_widgets:
spline_widget.Off()
self.spline_widgets.clear()
@_deprecate_positional_args(allowed=['mesh'])
def add_mesh_slice_spline( # noqa: PLR0917
self,
mesh,
generate_triangles: bool = False, # noqa: FBT001, FBT002
n_handles=5,
resolution=25,
widget_color=None,
show_ribbon: bool = False, # noqa: FBT001, FBT002
ribbon_color='pink',
ribbon_opacity=0.5,
initial_points=None,
closed: bool = False, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
**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 : 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 : InteractionEventType, optional
The VTK interaction event to use for triggering the
callback. Accepts either the strings ``'start'``, ``'end'``,
``'always'`` or a :vtk:`vtkCommand.EventIds`.
**kwargs : dict, optional
All additional keyword arguments are passed to
:func:`pyvista.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')
if name is None:
name = mesh.memory_address
rng = mesh.get_data_range(kwargs.get('scalars'))
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) # type: ignore[attr-defined]
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=_parse_interaction_event(interaction_event),
)
return self.add_mesh(alg, **kwargs) # type: ignore[attr-defined]
def add_measurement_widget(
self,
callback=None,
color=None,
):
"""Interactively measure distance with a distance widget.
Creates an overlay documenting the selected line and total
distance between two mouse left-click interactions.
The measurement overlay stays on the rendering until the
widget is deleted. Only one measurement can be added by each
widget instance.
Parameters
----------
callback : Callable[[tuple[float, float, float], [tuple[float, float, float], int], float]
The method called every time the widget calculates a
distance measurement. This callback receives the start
point and end point as cartesian coordinate tuples
and the calculated distance between the two points.
color : ColorLike, optional
The color of the measurement widget.
Returns
-------
:vtk:`vtkDistanceWidget`
The newly created distance widget.
See Also
--------
:ref:`distance_measurement_example`
"""
msg = 'Cannot add a widget to a closed plotter.'
iren = self._get_iren_not_none(msg) # type: ignore[attr-defined]
if color is None:
color = pyvista.global_theme.font.color.float_rgb
color = Color(color)
compute = lambda a, b: np.sqrt(np.sum((np.array(b) - np.array(a)) ** 2))
handle = _vtk.vtkPointHandleRepresentation3D()
representation = _vtk.vtkDistanceRepresentation3D()
representation.SetHandleRepresentation(handle)
widget = _vtk.vtkDistanceWidget()
widget.SetInteractor(iren.interactor)
widget.SetRepresentation(representation)
handle.GetProperty().SetColor(*color.float_rgb)
representation.GetLabelProperty().SetColor(*color.float_rgb)
representation.GetLineProperty().SetColor(*color.float_rgb)
iren.picker = PickerType.POINT
def place_point(*_):
p1 = [0, 0, 0]
p2 = [0, 0, 0]
representation.GetPoint1DisplayPosition(p1) # type: ignore[arg-type]
representation.GetPoint2DisplayPosition(p2) # type: ignore[arg-type]
if iren.picker.Pick(p1, self.renderer): # type: ignore[attr-defined]
pos1 = iren.picker.GetPickPosition()
representation.GetPoint1Representation().SetWorldPosition(pos1)
if iren.picker.Pick(p2, self.renderer): # type: ignore[attr-defined]
pos2 = iren.picker.GetPickPosition()
representation.GetPoint2Representation().SetWorldPosition(pos2)
representation.BuildRepresentation()
a = representation.GetPoint1Representation().GetWorldPosition()
b = representation.GetPoint2Representation().GetWorldPosition()
if callable(callback):
try_callback(callback, a, b, compute(a, b))
widget.AddObserver(_vtk.vtkCommand.EndInteractionEvent, place_point)
widget.On()
self.distance_widgets.append(widget)
return widget
def clear_measure_widgets(self):
"""Remove all of the measurement widgets."""
for distance_widget in self.distance_widgets:
distance_widget.Off()
self.distance_widgets.clear()
@_deprecate_positional_args(allowed=['callback'])
def add_sphere_widget( # noqa: PLR0917
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: bool = False, # noqa: FBT001, FBT002
test_callback: bool = True, # noqa: FBT001, FBT002
interaction_event: InteractionEventType = 'end',
):
"""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 : InteractionEventType, 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:`vtkSphereWidget`
The sphere widget.
See Also
--------
:ref:`sphere_widget_example`
"""
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):
point = widget.GetCenter()
index = widget.WIDGET_INDEX
if callable(callback):
args = [point, index] if num > 1 else [point]
if pass_widget:
args.append(widget)
try_callback(callback, *args)
if indices is None:
indices = list(range(num))
for i in range(num):
loc = center[i] if center.ndim > 1 else center
sphere_widget = _vtk.vtkSphereWidget()
sphere_widget.WIDGET_INDEX = indices[i] # type: ignore[attr-defined] # 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) # type: ignore[attr-defined]
sphere_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
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(
_parse_interaction_event(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."""
for sphere_widget in self.sphere_widgets:
sphere_widget.Off()
self.sphere_widgets.clear()
@_deprecate_positional_args(allowed=['actor'])
def add_affine_transform_widget( # noqa: PLR0917
self,
actor,
origin=None,
start: bool = True, # noqa: FBT001, FBT002
scale=0.15,
line_radius=0.02,
always_visible: bool = True, # noqa: FBT001, FBT002
axes_colors=None,
axes=None,
release_callback=None,
interact_callback=None,
):
"""Add a 3D affine transform widget.
This widget allows interactive transformations including translation and
rotation using the left mouse button.
Parameters
----------
actor : pyvista.Actor
The actor to which the widget is attached to.
origin : sequence[float], optional
Origin of the widget. Default is the origin of the main actor.
start : bool, default: True
If True, start the widget immediately.
scale : float, default: 0.15
Scale factor for the widget relative to the length of the actor.
line_radius : float, default: 0.02
Relative radius of the lines composing the widget.
always_visible : bool, default: True
Make the widget always visible. Setting this to ``False`` will cause
the widget geometry to be hidden by other actors in the plotter.
axes_colors : tuple[ColorLike], optional
Uses the theme by default. Configure the individual axis colors by
modifying either the theme with ``pyvista.global_theme.axes.x_color =
<COLOR>`` or setting this with a ``tuple`` as in ``('r', 'g', 'b')``.
axes : numpy.ndarray, optional
``(3, 3)`` Numpy array defining the X, Y, and Z axes. By default
this matches the default coordinate system.
release_callback : callable, optional
Call this method when releasing the left mouse button. It is passed
the ``user_matrix`` of the actor.
interact_callback : callable, optional
Call this method when moving the mouse with the left mouse button
pressed down and a valid movement actor selected. It is passed the
``user_matrix`` of the actor.
Returns
-------
pyvista.plotting.widgets.AffineWidget3D
The affine widget.
Notes
-----
After interacting with the actor, the transform will be stored within
:attr:`pyvista.Prop3D.user_matrix` but will not be applied to the
dataset. Use this matrix in conjunction with
:func:`pyvista.DataObjectFilters.transform` to transform the dataset.
Examples
--------
Add the 3d affine widget.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> widget = pl.add_affine_transform_widget(actor)
>>> pl.show()
Access the transform from the actor.
>>> actor.user_matrix
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
"""
return AffineWidget3D(
self,
actor,
origin=origin,
start=start,
scale=scale,
line_radius=line_radius,
always_visible=always_visible,
axes_colors=axes_colors,
axes=axes,
release_callback=release_callback,
interact_callback=interact_callback,
)
@_deprecate_positional_args(allowed=['callback'])
def add_checkbox_button_widget( # noqa: PLR0917
self,
callback,
value: bool = False, # noqa: FBT001, FBT002
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`.
"""
msg = 'Cannot add a widget to a closed plotter.'
self._get_iren_not_none(msg) # type: ignore[attr-defined]
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.ImageData(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) # type: ignore[attr-defined]
button_widget.SetRepresentation(button_rep)
button_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
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
@_deprecate_positional_args(allowed=['callback', 'radio_button_group'])
def add_radio_button_widget( # noqa: PLR0917
self,
callback,
radio_button_group,
value: bool = False, # noqa: FBT001, FBT002
title=None,
position=(10.0, 10.0),
size=50,
border_size=8,
color_on='blue',
color_off='grey',
background_color=None,
):
"""Add a radio button widget to the scene.
Radio buttons work in groups. Only one button in a group can be on at
at the same time. Typically you should add two or more buttons belonging
to a same radio button group. Each button should be passed a callback
function. This function will be called when a radio button in a group
is switched on, assuming it was not already on.
Parameters
----------
callback : callable
The method called when a radio button's state changes from off to
on.
radio_button_group: str
Name of the group for the radio button.
value : bool, default: False
The default state of the button. If multiple buttons in the same
group are initialized with to True state, only the last initialized
button will remain on.
title: str, optional
String title to be displayed next to the radio 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 diameter of the button in number of pixels.
border_size : int, default: 8
The size of the borders of the button in pixels.
color_on : ColorLike, default: ``'blue'``
The color used when the button is checked.
color_off : ColorLike, default: ``'grey'``
The color used when the button is not checked.
background_color : ColorLike, optional
The background color of the button. If not set, default will be set
as ``self.background_color``.
Returns
-------
:vtk:`vtkButtonWidget`
The VTK button widget configured as a radio button.
Examples
--------
The following example creates a background color switcher.
>>> import pyvista as pv
>>> p = pv.Plotter()
>>> def set_bg(color):
... def wrapped_callback():
... p.background_color = color
...
... return wrapped_callback
>>> _ = p.add_radio_button_widget(
... set_bg('white'),
... 'bgcolor',
... position=(10.0, 200.0),
... title='White',
... value=True,
... )
>>> _ = p.add_radio_button_widget(
... set_bg('lightblue'),
... 'bgcolor',
... position=(10.0, 140.0),
... title='Light Blue',
... )
>>> _ = p.add_radio_button_widget(
... set_bg('pink'),
... 'bgcolor',
... position=(10.0, 80.0),
... title='Pink',
... )
>>> p.show()
"""
msg = 'Cannot add a widget to a closed plotter.'
self._get_iren_not_none(msg) # type: ignore[attr-defined]
if radio_button_group not in self.radio_button_widget_dict:
self.radio_button_widget_dict[radio_button_group] = []
if title is not None:
if radio_button_group not in self.radio_button_title_dict:
self.radio_button_title_dict[radio_button_group] = []
button_title = self.add_text( # type: ignore[attr-defined]
title,
position=(position[0] + size + 10.0, position[1] + 7.5),
font_size=15,
)
self.radio_button_title_dict[radio_button_group].append(button_title)
color_on = Color(color_on)
color_off = Color(color_off)
background_color = Color(background_color, default_color=self.background_color) # type: ignore[attr-defined]
def create_radio_button(fg_color, bg_color, size=size, smooth=2): # noqa: PLR0917
fg_color = np.array(fg_color.int_rgb)
bg_color = np.array(bg_color.int_rgb)
n_points = size**2
button = pyvista.ImageData(dimensions=(size, size, 1))
arr = np.array([bg_color] * n_points).reshape(size, size, 3) # fill background
centre = size / 2
rad_outer = centre
rad_inner = centre - border_size
# Paint radio button with simple anti-aliasing
for i, j in product(range(size), range(size)):
distance = np.sqrt((i - size / 2) ** 2 + (j - size / 2) ** 2)
if distance < rad_inner:
arr[i, j] = fg_color
elif rad_inner <= distance <= rad_inner + smooth:
blend = (distance - rad_inner) / smooth
arr[i, j] = (1 - blend) * fg_color + blend * bg_color
elif rad_outer - 2 * smooth <= distance <= rad_outer:
blend = abs(distance - rad_outer + smooth) / smooth
arr[i, j] = (1 - blend) * fg_color + blend * bg_color
button.point_data['texture'] = arr.reshape(n_points, 3).astype(np.uint8)
return button
button_on = create_radio_button(color_on, background_color)
button_off = create_radio_button(color_off, background_color)
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_rep.GetProperty().SetColor((1, 1, 1))
button_widget = _vtk.vtkButtonWidget()
button_widget.SetInteractor(self.iren.interactor) # type: ignore[attr-defined]
button_widget.SetRepresentation(button_rep)
button_widget.SetCurrentRenderer(self.renderer) # type: ignore[attr-defined]
button_widget.On()
def toggle_other_buttons_off(widget):
other_buttons = [
w for w in self.radio_button_widget_dict[radio_button_group] if w is not widget
]
for w in other_buttons:
w.GetRepresentation().SetState(0)
def _the_callback(widget, _event):
widget_rep = widget.GetRepresentation()
state = widget_rep.GetState()
# Toggle back on, if button was already on, and was clicked off
if not state:
widget_rep.SetState(1)
state = True
else:
toggle_other_buttons_off(widget)
if callable(callback):
try_callback(callback)
button_widget.AddObserver(_vtk.vtkCommand.StateChangedEvent, _the_callback)
self.radio_button_widget_dict[radio_button_group].append(button_widget)
if value:
toggle_other_buttons_off(button_widget)
return button_widget
def clear_radio_button_widgets(self):
"""Remove all of the radio button widgets."""
for widgets in self.radio_button_widget_dict.values():
for widget in widgets:
widget.Off()
self.radio_button_widget_dict.clear()
for titles in self.radio_button_title_dict.values():
for title in titles:
title.VisibilityOff()
self.radio_button_title_dict.clear()
@_deprecate_positional_args
def add_camera_orientation_widget(self, animate: bool = True, n_frames=20): # noqa: FBT001, FBT002
"""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
-------
:vtk:`vtkCameraOrientationWidget`
Camera orientation widget.
See Also
--------
:meth:`~pyvista.Plotter.add_axes`
Add arrow-style axes as an orientation widget.
:meth:`~pyvista.Plotter.add_box_axes`
Add an axes box as an orientation widget.
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Add a camera orientation widget to the scene.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> plotter = pv.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
msg = 'vtkCameraOrientationWidget requires vtk>=9.1.0'
raise VTKVersionError(msg)
widget = vtkCameraOrientationWidget()
widget.SetParentRenderer(self.renderer) # type: ignore[attr-defined]
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."""
for camera_widget in self.camera_widgets:
camera_widget.Off()
self.camera_widgets.clear()
def clear_button_widgets(self):
"""Remove all of the button widgets."""
for button_widget in self.button_widgets:
button_widget.Off()
self.button_widgets.clear()
@_deprecate_positional_args(allowed=['logo'])
def add_logo_widget( # noqa: PLR0917
self,
logo: pyvista.ImageData | str | pathlib.Path | None = None,
position: VectorLike[float] = (0.75, 0.8),
size: VectorLike[float] = (0.2, 0.2),
opacity: float = 1.0,
):
"""Add a logo widget to the top of the viewport.
If no logo is passed, the PyVista logo will be used.
Parameters
----------
logo : pyvista.ImageData or pathlib.Path, optional
The logo to display. If a pathlike is passed, it is assumed to be a
file path to an image.
position : tuple(float), optional
The position of the logo in the viewport. The first value is the
horizontal position and the second value is the vertical position.
Both values must be between 0 and 1.
size : tuple(float), optional
The size of the logo in the viewport. The first value is the
horizontal size and the second value is the vertical size. Both
values must be between 0 and 1.
opacity : float, optional
The opacity of the logo. Must be between 0 and 1.
Returns
-------
:vtk:`vtkLogoWidget`
The logo widget.
Examples
--------
Add a logo widget to the scene.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> _ = pl.add_logo_widget()
>>> _ = pl.add_mesh(pv.Sphere(), show_edges=True)
>>> pl.show()
"""
if logo is None:
logo = pyvista.global_theme.logo_file
if logo is None:
# Fallback to PyVista logo
from pyvista import examples
logo = examples.logofile
# Read dataset and narrow the logo type to ImageData
logo_maybe: pyvista.DataObject | str | pathlib.Path | None
logo_maybe = pyvista.read(logo) if isinstance(logo, (str, pathlib.Path)) else logo
if not isinstance(logo_maybe, pyvista.ImageData):
msg = 'Logo must be a pyvista.ImageData or a file path to an image.'
raise TypeError(msg)
else:
logo = logo_maybe
representation = _vtk.vtkLogoRepresentation()
representation.SetImage(logo)
representation.SetPosition(*position)
representation.SetPosition2(*size)
representation.GetImageProperty().SetOpacity(opacity)
widget = _vtk.vtkLogoWidget()
widget.SetInteractor(self.iren.interactor) # type: ignore[attr-defined]
widget.SetRepresentation(representation)
widget.On()
self.logo_widgets.append(widget)
return widget
def clear_logo_widgets(self):
"""Remove all of the logo widgets."""
for logo_widget in self.logo_widgets:
logo_widget.Off()
self.logo_widgets.clear()
def add_camera3d_widget(self):
"""Add a camera3d widget allow to move the camera.
.. note::
This widget requires ``vtk>=9.3.0``.
Returns
-------
:vtk:`vtkCamera3DWidget`
The camera3d widget.
Examples
--------
Add a camera3d widget to the scene.
>>> import pyvista as pv
>>> sphere = pv.Sphere()
>>> plotter = pv.Plotter(shape=(1, 2))
>>> _ = plotter.add_mesh(sphere, show_edges=True)
>>> plotter.subplot(0, 1)
>>> _ = plotter.add_mesh(sphere, show_edges=True)
>>> _ = plotter.add_camera3d_widget()
>>> plotter.show(cpos=plotter.camera_position)
"""
try:
from vtkmodules.vtkInteractionWidgets import vtkCamera3DRepresentation
from vtkmodules.vtkInteractionWidgets import vtkCamera3DWidget
except ImportError: # pragma: no cover
from pyvista.core.errors import VTKVersionError
msg = 'vtkCamera3DWidget requires vtk>=9.3.0'
raise VTKVersionError(msg)
representation = vtkCamera3DRepresentation()
representation.SetCamera(self.renderer.GetActiveCamera()) # type: ignore[attr-defined]
widget = vtkCamera3DWidget()
widget.SetInteractor(self.iren.interactor) # type: ignore[attr-defined]
widget.SetRepresentation(representation)
widget.On()
self.camera3d_widgets.append(widget)
return widget
def clear_camera3d_widgets(self):
"""Remove all of the camera3d widgets."""
for camera3d_widget in self.camera3d_widgets:
camera3d_widget.Off()
self.camera3d_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_radio_button_widgets()
self.clear_camera_widgets()
self.clear_measure_widgets()
self.clear_logo_widgets()
self.clear_camera3d_widgets()