"""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 `. 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 `. 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 `. 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 = `` 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()