"""Module managing picking events.""" from __future__ import annotations from functools import partial from functools import wraps import warnings import weakref import numpy as np import pyvista from pyvista._deprecate_positional_args import _deprecate_positional_args from pyvista.core.errors import PyVistaDeprecationWarning from pyvista.core.utilities.misc import _NoNewAttrMixin from pyvista.core.utilities.misc import abstract_class from pyvista.core.utilities.misc import try_callback from . import _vtk from .composite_mapper import CompositePolyDataMapper from .errors import PyVistaPickingError from .mapper import _mapper_get_data_set_input from .mapper import _mapper_has_data_set_input from .opts import ElementType from .opts import PickerType PICKED_REPRESENTATION_NAMES = { 'point': '_picked_point', 'mesh': '_picked_mesh', 'through': '_picked_through_selection', 'visible': '_picked_visible_selection', 'element': '_picked_element', 'path': '_picked_path', 'horizon': '_picked_horizon', 'frustum': '_rectangle_selection_frustum', } def _launch_pick_event(interactor, _event): """Create a Pick event based on coordinate or left-click.""" click_x, click_y = interactor.GetEventPosition() click_z = 0 picker = interactor.GetPicker() renderer = interactor.GetInteractorStyle()._parent()._plotter.iren.get_poked_renderer() picker.Pick(click_x, click_y, click_z, renderer) def _poked_context_callback(plotter, *args, **kwargs): """Use _poked_context_callback in a poked renderer context.""" with plotter.iren.poked_subplot(): try_callback(*args, **kwargs) class RectangleSelection(_NoNewAttrMixin): """Internal data structure for rectangle based selections. Parameters ---------- frustum : :vtk:`vtkPlanes` Frustum that defines the selection. viewport : tuple[float, float, float, float] The selected viewport coordinates, given as ``(x0, y0, x1, y1)``. """ def __init__(self, frustum, viewport): self._frustum = frustum self._viewport = viewport @property def frustum(self) -> _vtk.vtkPlanes: # numpydoc ignore=RT01 """Get the selected frustum through the scene.""" return self._frustum @property def frustum_mesh(self) -> pyvista.PolyData: # numpydoc ignore=RT01 """Get the frustum as a PyVista mesh.""" frustum_source = _vtk.vtkFrustumSource() frustum_source.ShowLinesOff() frustum_source.SetPlanes(self.frustum) frustum_source.Update() return pyvista.wrap(frustum_source.GetOutput()) @property def viewport(self) -> tuple[float, float, float, float]: # numpydoc ignore=RT01 """Get the selected viewport coordinates. Coordinates are given as: ``(x0, y0, x1, y1)`` """ return self._viewport class PointPickingElementHandler(_NoNewAttrMixin): """Internal picking handler for element-based picking. This handler is only valid for single point picking operations. Parameters ---------- mode : ElementType, optional The element type to pick. callback : callable, optional A callback function to be executed on picking events. """ def __init__(self, mode: ElementType = ElementType.CELL, callback=None): self._picker_ = None self.callback = callback self.mode = ElementType.from_any(mode) @property def picker(self): # numpydoc ignore=RT01 """Get or set the picker instance.""" return self._picker_() # type: ignore[misc] @picker.setter def picker(self, picker): self._picker_ = weakref.ref(picker) # type: ignore[assignment] def get_mesh(self): """Get the picked mesh. Returns ------- pyvista.DataSet Picked mesh. """ ds = self.picker.GetDataSet() if ds is not None: return pyvista.wrap(ds) return None def get_cell(self, picked_point): """Get the picked cell of the picked mesh. Parameters ---------- picked_point : sequence[float] Coordinates of the picked point. Returns ------- pyvista.UnstructuredGrid UnstructuredGrid containing the picked cell. """ mesh = self.get_mesh() # cell_id = self.picker.GetCellId() cell_id = mesh.find_containing_cell(picked_point) # more accurate if cell_id < 0: return None # TODO: this happens but shouldn't # pragma: no cover cell = mesh.extract_cells(cell_id) cell.cell_data['vtkOriginalCellIds'] = np.array([cell_id]) return cell def get_face(self, picked_point): """Get the picked face of the picked cell. Parameters ---------- picked_point : sequence[float] Coordinates of the picked point. Returns ------- pyvista.UnstructuredGrid UnstructuredGrid containing the picked face. """ cell = self.get_cell(picked_point).get_cell(0) if cell.n_faces > 1: for face in cell.faces: contains = face.cast_to_unstructured_grid().find_containing_cell(picked_point) if contains > -1: break if contains < 0: # this shouldn't happen msg = 'Trouble aligning point with face.' raise RuntimeError(msg) face = face.cast_to_unstructured_grid() face.field_data['vtkOriginalFaceIds'] = np.array([len(cell.faces) - 1]) else: face = cell.cast_to_unstructured_grid() face.field_data['vtkOriginalFaceIds'] = np.array([0]) return face def get_edge(self, picked_point): """Get the picked edge of the picked cell. Parameters ---------- picked_point : sequence[float] Coordinates of the picked point. Returns ------- pyvista.UnstructuredGrid UnstructuredGrid containing the picked edge. """ cell = self.get_cell(picked_point).get_cell(0) if cell.n_edges > 1: ei = ( cell.cast_to_unstructured_grid() .extract_all_edges() .find_closest_cell(picked_point) ) edge = cell.edges[ei].cast_to_unstructured_grid() edge.field_data['vtkOriginalEdgeIds'] = np.array([ei]) else: edge = cell.cast_to_unstructured_grid() return edge def get_point(self, picked_point): """Get the picked point of the picked mesh. Parameters ---------- picked_point : sequence[float] Coordinates of the picked point. Returns ------- pyvista.PolyData Picked mesh containing the point. """ mesh = self.get_mesh() pid = mesh.find_closest_point(picked_point) picked = pyvista.PolyData(mesh.points[pid]) picked.point_data['vtkOriginalPointIds'] = np.array([pid]) return picked def __call__(self, picked_point, picker): """Perform the pick.""" self.picker = picker mesh = self.get_mesh() if mesh is None: return # No selected mesh (point not on surface of mesh) if self.mode == ElementType.MESH: picked = mesh elif self.mode == ElementType.CELL: picked = self.get_cell(picked_point) if picked is None: return # TODO: handle elif self.mode == ElementType.FACE: picked = self.get_face(picked_point) elif self.mode == ElementType.EDGE: picked = self.get_edge(picked_point) elif self.mode == ElementType.POINT: picked = self.get_point(picked_point) if self.callback: try_callback(self.callback, picked) @abstract_class class PickingInterface: # numpydoc ignore=PR01 """An internal class to hold core picking related features.""" def __init__(self, *args, **kwargs): """Initialize the picking interface.""" super().__init__(*args, **kwargs) self._picking_left_clicking_observer = None self._picking_right_clicking_observer = None self._picker_in_use = False self._picked_point = None def _clear_picking_representations(self): """Clear all picking representations.""" for name in PICKED_REPRESENTATION_NAMES.values(): self.remove_actor(name) # type: ignore[attr-defined] @property def picked_point(self): # numpydoc ignore=RT01 """Return the picked point. This returns the picked point after selecting a point. Returns ------- numpy.ndarray or None Picked point if available. """ return self._picked_point def get_pick_position(self): """Get the pick position or area. Returns ------- sequence Picked position or area as ``(x0, y0, x1, y1)``. """ renderer = self.iren.get_poked_renderer() # type: ignore[attr-defined] return renderer.get_pick_position() def pick_click_position(self): """Get corresponding click location in the 3D plot. Returns ------- tuple Three item tuple with the 3D picked position. """ if self.click_position is None: # type: ignore[attr-defined] self.store_click_position() # type: ignore[attr-defined] renderer = self.iren.get_poked_renderer() # type: ignore[attr-defined] self.iren.picker.Pick(self.click_position[0], self.click_position[1], 0, renderer) # type: ignore[attr-defined] return self.iren.picker.GetPickPosition() # type: ignore[attr-defined] def pick_mouse_position(self): """Get corresponding mouse location in the 3D plot. Returns ------- tuple Three item tuple with the 3D picked position. """ if self.mouse_position is None: # type: ignore[attr-defined] self.store_mouse_position() # type: ignore[attr-defined] renderer = self.iren.get_poked_renderer() # type: ignore[attr-defined] self.iren.picker.Pick(self.mouse_position[0], self.mouse_position[1], 0, renderer) # type: ignore[attr-defined] return self.iren.picker.GetPickPosition() # type: ignore[attr-defined] def _init_click_picking_callback(self, *, left_clicking=False): if left_clicking: self._picking_left_clicking_observer = self.iren.add_observer( # type: ignore[attr-defined] 'LeftButtonPressEvent', partial(try_callback, _launch_pick_event), ) else: self._picking_right_clicking_observer = self.iren.add_observer( # type: ignore[attr-defined] 'RightButtonPressEvent', partial(try_callback, _launch_pick_event), ) def disable_picking(self) -> None: """Disable any active picking and remove observers. Examples -------- Enable and then disable picking. >>> import pyvista as pv >>> mesh = pv.Sphere(center=(1, 0, 0)) >>> cube = pv.Cube() >>> pl = pv.Plotter() >>> _ = pl.add_mesh(mesh) >>> _ = pl.add_mesh(cube) >>> _ = pl.enable_mesh_picking() >>> pl.disable_picking() """ # remove left and right clicking observer if available if getattr(self, 'iren', None): self.iren.remove_observer(self._picking_left_clicking_observer) # type: ignore[attr-defined] self.iren.remove_observer(self._picking_right_clicking_observer) # type: ignore[attr-defined] # Reset to default picker self.iren.reset_picker() # type: ignore[attr-defined] self._picking_left_clicking_observer = None self._picking_right_clicking_observer = None self._picker_in_use = False def _validate_picker_not_in_use(self): if self._picker_in_use: msg = ( 'Picking is already enabled, please disable previous picking ' 'with `disable_picking()`.' ) raise PyVistaPickingError(msg) @_deprecate_positional_args(allowed=['callback']) def enable_point_picking( # noqa: PLR0917 self, callback=None, tolerance=0.025, left_clicking=False, # noqa: FBT002 picker=PickerType.POINT, show_message=True, # noqa: FBT002 font_size=18, color='pink', point_size=10, show_point=True, # noqa: FBT002 use_picker=False, # noqa: FBT002 pickable_window=False, # noqa: FBT002 clear_on_no_selection=True, # noqa: FBT002 **kwargs, ): """Enable picking at points under the cursor. Enable picking a point at the mouse location in the render view using the right mouse button. This point is saved to the ``.picked_point`` attribute on the plotter. Pass a callback that takes that point as an argument. The picked point can either be a point on the first intersecting mesh, or a point in the 3D window. The ``picker`` choice will help determine how the point picking is performed. Parameters ---------- callback : callable, optional When input, calls this callable after a pick is made. The picked point is input as the first parameter to this callable. tolerance : float, tolerance: 0.025 Specify tolerance for performing pick operation. Tolerance is specified as fraction of rendering window size. Rendering window size is measured across diagonal. This is only valid for some choices of ``picker``. left_clicking : bool, default: False When ``True``, points can be picked by clicking the left mouse button. Default is to use the right mouse button. picker : str | PickerType, optional Choice of VTK picker class type: * ``'hardware'``: Uses :vtk:`vtkHardwarePicker` which is more performant for large geometries (default). * ``'cell'``: Uses :vtk:`vtkCellPicker`. * ``'point'``: Uses :vtk:`vtkPointPicker` which will snap to points on the surface of the mesh. * ``'volume'``: Uses :vtk:`vtkVolumePicker`. show_message : bool | str, default: True Show the message about how to use the point picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the size of the message. color : ColorLike, default: "pink" The color of the selected mesh when shown. point_size : int, default: 10 Size of picked points if ``show_point`` is ``True``. show_point : bool, default: True Show the picked point after clicking. use_picker : bool, default: False When ``True``, the callback will also be passed the picker. pickable_window : bool, default: False When ``True`` and the chosen picker supports it, points in the 3D window are pickable. clear_on_no_selection : bool, default: True Clear the selections when no point is selected. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked point is interactively displayed. Examples -------- Enable point picking with a custom message. >>> import pyvista as pv >>> pl = pv.Plotter() >>> _ = pl.add_mesh(pv.Sphere()) >>> _ = pl.add_mesh(pv.Cube(), pickable=False) >>> pl.enable_point_picking(show_message='Pick a point') See :ref:`point_picking_example` for a full example using this method. """ self._validate_picker_not_in_use() if 'use_mesh' in kwargs: warnings.warn( '`use_mesh` is deprecated. See `use_picker` instead.', PyVistaDeprecationWarning, ) use_mesh = kwargs.pop('use_mesh') else: use_mesh = False self_ = weakref.ref(self) def _end_pick_event(picker, _event): if ( not pickable_window and hasattr(picker, 'GetDataSet') and picker.GetDataSet() is None ): # Clear the selection self._picked_point = None if clear_on_no_selection: with self_().iren.poked_subplot(): # type: ignore[union-attr] self_()._clear_picking_representations() # type: ignore[union-attr] return with self_().iren.poked_subplot(): # type: ignore[union-attr] point = np.array(picker.GetPickPosition()) point /= self_().scale # type: ignore[union-attr] # HACK: handle scale self_()._picked_point = point # type: ignore[union-attr] if show_point: _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] self_().picked_point, # type: ignore[union-attr] color=color, point_size=point_size, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['point']), pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callable(callback): if use_picker: _poked_context_callback(self_(), callback, self.picked_point, picker) elif use_mesh: # Lower priority _poked_context_callback( self_(), callback, picker.GetDataSet(), picker.GetPointId(), ) else: _poked_context_callback(self_(), callback, self.picked_point) if picker is not None: # If None, that means use already set picker self.iren.picker = picker # type: ignore[attr-defined] if hasattr(self.iren.picker, 'SetTolerance'): # type: ignore[attr-defined] self.iren.picker.SetTolerance(tolerance) # type: ignore[attr-defined] self.iren.add_pick_observer(_end_pick_event) # type: ignore[attr-defined] self._init_click_picking_callback(left_clicking=left_clicking) self._picker_in_use = True # Now add text about cell-selection if show_message: if show_message is True: show_message = 'Left-click' if left_clicking else 'Right-click' show_message += ' or press P to pick under the mouse' self._picking_text = self.add_text( # type: ignore[attr-defined] str(show_message), font_size=font_size, name='_point_picking_message', ) @_deprecate_positional_args(allowed=['callback']) def enable_rectangle_picking( # noqa: PLR0917 self, callback=None, show_message=True, # noqa: FBT002 font_size=18, start=False, # noqa: FBT002 show_frustum=False, # noqa: FBT002 style='wireframe', color='pink', **kwargs, ): """Enable rectangle based picking at cells. Press ``"r"`` to enable rectangle based selection. Press ``"r"`` again to turn it off. Picking with the rectangle selection tool provides two values that are passed as the ``RectangleSelection`` object in the callback: 1. ``RectangleSelection.viewport``: the viewport coordinates of the selection rectangle. 2. ``RectangleSelection.frustum``: the full frustum made from the selection rectangle into the scene. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The ``RectangleSelection`` is the only passed argument containing the viewport coordinates of the selection and the projected frustum. show_message : bool | str, default: True Show the message about how to use the cell picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. start : bool, default: True Automatically start the cell selection tool. show_frustum : bool, default: False Show the frustum in the scene. style : str, default: "wireframe" Visualization style of the selection frustum. One of the following: ``style='surface'``, ``style='wireframe'``, or ``style='points'``. color : ColorLike, default: "pink" The color of the selected frustum when shown. **kwargs : dict, optional All remaining keyword arguments are used to control how the selection frustum is interactively displayed. Examples -------- Add a mesh and a cube to a plot and enable cell picking. >>> import pyvista as pv >>> mesh = pv.Sphere(center=(1, 0, 0)) >>> cube = pv.Cube() >>> pl = pv.Plotter() >>> _ = pl.add_mesh(mesh) >>> _ = pl.add_mesh(cube) >>> _ = pl.enable_rectangle_picking() """ self._validate_picker_not_in_use() self_ = weakref.ref(self) def _end_pick_helper(picker, *_): renderer = picker.GetRenderer() # TODO: double check this is poked renderer x0 = int(renderer.GetPickX1()) x1 = int(renderer.GetPickX2()) y0 = int(renderer.GetPickY1()) y1 = int(renderer.GetPickY2()) selection = RectangleSelection(frustum=picker.GetFrustum(), viewport=(x0, y0, x1, y1)) if show_frustum: with self_().iren.poked_subplot(): # type: ignore[union-attr] _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] selection.frustum_mesh, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['frustum']), style=style, color=color, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callback is not None: _poked_context_callback(self_(), callback, selection) self.enable_rubber_band_style() # type: ignore[attr-defined] # TODO: better handle? self.iren.picker = 'rendered' # type: ignore[attr-defined] self.iren.add_pick_observer(_end_pick_helper) # type: ignore[attr-defined] self._picker_in_use = True # Now add text about cell-selection if show_message: if show_message is True: show_message = 'Press R to toggle selection tool' self._picking_text = self.add_text( # type: ignore[attr-defined] str(show_message), font_size=font_size, name='_rectangle_picking_message', ) if start: self.iren._style_class.StartSelect() # type: ignore[attr-defined] @abstract_class class PickingMethods(PickingInterface): # numpydoc ignore=PR01 """Internal class to contain picking utilities.""" def __init__(self, *args, **kwargs): """Initialize the picking methods.""" super().__init__(*args, **kwargs) self._picked_actor = None self._picked_mesh = None self._picked_cell = None self._picking_text = None self._picked_block_index = None @property def picked_actor(self): # numpydoc ignore=RT01 """Return the picked mesh. This returns the picked actor after selecting a mesh with :func:`enable_surface_point_picking ` or :func:`enable_mesh_picking `. Returns ------- pyvista.Actor or None Picked actor if available. """ return self._picked_actor @property def picked_mesh(self): # numpydoc ignore=RT01 """Return the picked mesh. This returns the picked mesh after selecting a mesh with :func:`enable_surface_point_picking ` or :func:`enable_mesh_picking `. Returns ------- pyvista.DataSet or None Picked mesh if available. """ return self._picked_mesh @property def picked_cell(self): # numpydoc ignore=RT01 """Return the picked cell. This returns the picked cell after selecting a cell. Returns ------- pyvista.Cell or None Picked cell if available. """ return self._picked_cell @property def picked_cells(self): # numpydoc ignore=RT01 """Return the picked cells. This returns the picked cells after selecting cells. Returns ------- pyvista.Cell or None Picked cell if available. """ return self._picked_cell @property def picked_block_index(self): # numpydoc ignore=RT01 """Return the picked block index. This returns the picked block index after selecting a point with :func:`enable_point_picking `. Returns ------- int or None Picked block if available. If ``-1``, then a non-composite dataset was selected. """ return self._picked_block_index @wraps(PickingInterface.disable_picking) def disable_picking(self) -> None: # type: ignore[override] """Disable picking.""" super().disable_picking() # remove any picking text if hasattr(self, 'renderers'): for renderer in self.renderers: renderer.remove_actor(self._picking_text, render=False) self._picking_text = None @_deprecate_positional_args(allowed=['callback']) def enable_surface_point_picking( # noqa: PLR0917 self, callback=None, show_message=True, # noqa: FBT002 font_size=18, color='pink', show_point=True, # noqa: FBT002 point_size=10, tolerance=0.025, pickable_window=False, # noqa: FBT002 left_clicking=False, # noqa: FBT002 picker=PickerType.CELL, use_picker=False, # noqa: FBT002 clear_on_no_selection=True, # noqa: FBT002 **kwargs, ): """Enable picking of a point on the surface of a mesh. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The ``mesh`` is input as the first parameter to this callable. show_message : bool | str, default: True Show the message about how to use the mesh picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. color : ColorLike, default: "pink" The color of the selected mesh when shown. show_point : bool, default: True Show the selection interactively. point_size : int, default: 10 Size of picked points if ``show_point`` is ``True``. tolerance : float, default: 0.025 Specify tolerance for performing pick operation. Tolerance is specified as fraction of rendering window size. Rendering window size is measured across diagonal. .. warning:: This is ignored with the ``'hardware'`` ``picker``. pickable_window : bool, default: False When ``True``, points in the 3D window are pickable. left_clicking : bool, default: False When ``True``, meshes can be picked by clicking the left mousebutton. .. note:: If enabled, left-clicking will **not** display the bounding box around the picked mesh. picker : str | PickerType, optional Choice of VTK picker class type: * ``'hardware'``: Uses :vtk:`vtkHardwarePicker` which is more performant for large geometries (default). * ``'cell'``: Uses :vtk:`vtkCellPicker`. * ``'point'``: Uses :vtk:`vtkPointPicker` which will snap to points on the surface of the mesh. * ``'volume'``: Uses :vtk:`vtkVolumePicker`. use_picker : bool, default: False When ``True``, the callback will also be passed the picker. clear_on_no_selection : bool, default: True Clear the selections when no point is selected. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. Notes ----- Picked point can be accessed from :attr:`picked_point ` attribute. Examples -------- Add a cube to a plot and enable cell picking. >>> import pyvista as pv >>> cube = pv.Cube() >>> pl = pv.Plotter() >>> _ = pl.add_mesh(cube) >>> _ = pl.enable_surface_point_picking() See :ref:`surface_point_picking_example` for a full example using this method. """ # only allow certain pickers to be used for surface picking # the picker class needs to have `GetDataSet()` picker = PickerType.from_any(picker) valid_pickers = [ PickerType.POINT, PickerType.CELL, PickerType.HARDWARE, PickerType.VOLUME, ] if picker not in valid_pickers: msg = f'Invalid picker choice for surface picking. Use one of: {valid_pickers}' raise ValueError(msg) self_ = weakref.ref(self) def _end_pick_event(picked_point, picker): if not pickable_window and picker.GetActor() is None: self_()._picked_point = None # type: ignore[union-attr] self_()._picked_actor = None # type: ignore[union-attr] self_()._picked_mesh = None # type: ignore[union-attr] if clear_on_no_selection: with self_().iren.poked_subplot(): # type: ignore[union-attr] self_()._clear_picking_representations() # type: ignore[union-attr] return self_()._picked_actor = picker.GetActor() # type: ignore[union-attr] self_()._picked_mesh = picker.GetDataSet() # type: ignore[union-attr] if show_point: with self_().iren.poked_subplot(): # type: ignore[union-attr] _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] picked_point, color=color, point_size=point_size, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['point']), pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callable(callback): if use_picker: _poked_context_callback(self_(), callback, picked_point, picker) else: _poked_context_callback(self_(), callback, picked_point) self.enable_point_picking( callback=_end_pick_event, picker=picker, show_point=False, show_message=show_message, left_clicking=left_clicking, use_picker=True, font_size=font_size, tolerance=tolerance, pickable_window=True, # let this callback handle pickable window clear_on_no_selection=clear_on_no_selection, ) @_deprecate_positional_args(allowed=['callback']) def enable_mesh_picking( # noqa: PLR0917 self, callback=None, show=True, # noqa: FBT002 show_message=True, # noqa: FBT002 style='wireframe', line_width=5, color='pink', font_size=18, left_clicking=False, # noqa: FBT002 use_actor=False, # noqa: FBT002 picker=PickerType.CELL, **kwargs, ): """Enable picking of a mesh. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The ``mesh`` is input as the first parameter to this callable. show : bool, default: True Show the selection interactively. Best when combined with ``left_clicking``. show_message : bool | str, default: True Show the message about how to use the mesh picking tool. If this is a string, that will be the message shown. style : str, default: "wireframe" Visualization style of the selection. One of the following: * ``'surface'`` * ``'wireframe'`` * ``'points'`` line_width : float, default: 5.0 Thickness of selected mesh edges. color : ColorLike, default: "pink" The color of the selected mesh when shown. font_size : int, default: 18 Sets the font size of the message. left_clicking : bool, default: False When ``True``, meshes can be picked by clicking the left mousebutton. .. note:: If enabled, left-clicking will **not** display the bounding box around the picked point. use_actor : bool, default: False If True, the callback will be passed the picked actor instead of the mesh object. picker : str | PickerType, optional Choice of VTK picker class type: * ``'hardware'``: Uses :vtk:`vtkHardwarePicker` which is more performant for large geometries (default). * ``'cell'``: Uses :vtk:`vtkCellPicker`. * ``'point'``: Uses :vtk:`vtkPointPicker` which will snap to points on the surface of the mesh. * ``'volume'``: Uses :vtk:`vtkVolumePicker`. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. Returns ------- :vtk:`vtkPropPicker` Property picker. Examples -------- Add a sphere and a cube to a plot and enable mesh picking. Enable ``left_clicking`` to immediately start picking on the left click and disable showing the box. You can still press the ``p`` key to select meshes. >>> import pyvista as pv >>> mesh = pv.Sphere(center=(1, 0, 0)) >>> cube = pv.Cube() >>> pl = pv.Plotter() >>> _ = pl.add_mesh(mesh) >>> _ = pl.add_mesh(cube) >>> _ = pl.enable_mesh_picking() See :ref:`mesh_picking_example` for a full example using this method. """ self_ = weakref.ref(self) def end_pick_call_back(*args): # noqa: ARG001 if callback: if use_actor: _poked_context_callback(self_(), callback, self_()._picked_actor) # type: ignore[union-attr] else: _poked_context_callback(self_(), callback, self_()._picked_mesh) # type: ignore[union-attr] if show: # Select the renderer where the mesh is added. active_renderer_index = self_().renderers._active_index # type: ignore[union-attr] loc = self_().iren.get_event_subplot_loc() # type: ignore[union-attr] self_().subplot(*loc) # type: ignore[union-attr] # Use try in case selection is empty or invalid try: with self_().iren.poked_subplot(): # type: ignore[union-attr] _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] self_()._picked_mesh, # type: ignore[union-attr] name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['mesh']), style=style, color=color, line_width=line_width, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) except Exception as e: # noqa: BLE001 # pragma: no cover warnings.warn('Unable to show mesh when picking:\n\n%s', str(e)) # type: ignore[call-overload] # Reset to the active renderer. loc = self_().renderers.index_to_loc(active_renderer_index) # type: ignore[union-attr] self_().subplot(*loc) # type: ignore[union-attr] # render here prior to running the callback self_().render() # type: ignore[union-attr] # add on-screen message about point-selection if show_message and show_message is True: show_message = 'Left-click' if left_clicking else 'Right-click' show_message += ' or press P to pick single dataset under the mouse pointer' self.enable_surface_point_picking( callback=end_pick_call_back, picker=picker, show_point=False, show_message=show_message, left_clicking=left_clicking, use_picker=True, font_size=font_size, pickable_window=False, ) @_deprecate_positional_args(allowed=['callback']) def enable_rectangle_through_picking( # noqa: PLR0917 self, callback=None, show=True, # noqa: FBT002 style='wireframe', line_width=5, color='pink', show_message=True, # noqa: FBT002 font_size=18, start=False, # noqa: FBT002 show_frustum=False, # noqa: FBT002 **kwargs, ): """Enable rectangle based cell picking through the scene. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The picked cells is the only passed argument. show : bool, default: True Show the selection interactively. style : str, default: "wireframe" Visualization style of the selection frustum. One of the following: ``style='surface'``, ``style='wireframe'``, or ``style='points'``. line_width : float, default: 5.0 Thickness of selected mesh edges. color : ColorLike, default: "pink" The color of the selected frustum when shown. show_message : bool | str, default: True Show the message about how to use the cell picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. start : bool, default: True Automatically start the cell selection tool. show_frustum : bool, default: False Show the frustum in the scene. **kwargs : dict, optional All remaining keyword arguments are used to control how the selection frustum is interactively displayed. """ self_ = weakref.ref(self) def finalize(picked): if picked is None: # Indicates invalid pick with self_().iren.poked_subplot(): # type: ignore[union-attr] self_()._clear_picking_representations() # type: ignore[union-attr] return self._picked_cell = picked if show: # Use try in case selection is empty with self_().iren.poked_subplot(): # type: ignore[union-attr] _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] picked, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['through']), style=style, color=color, line_width=line_width, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callback is not None: _poked_context_callback(self_(), callback, self_().picked_cells) # type: ignore[union-attr] def through_pick_callback(selection): picked = pyvista.MultiBlock() renderer = self_().iren.get_poked_renderer() # type: ignore[union-attr] for actor in renderer.actors.values(): if ( (mapper := actor.GetMapper()) and _mapper_has_data_set_input(mapper) and actor.GetPickable() ): input_mesh = pyvista.wrap(_mapper_get_data_set_input(actor.GetMapper())) input_mesh.cell_data['orig_extract_id'] = np.arange(input_mesh.n_cells) extract = _vtk.vtkExtractGeometry() extract.SetInputData(input_mesh) extract.SetImplicitFunction(selection.frustum) extract.Update() picked.append(pyvista.wrap(extract.GetOutput())) if picked.n_blocks == 0 or picked.combine().n_cells < 1: self_()._picked_cell = None # type: ignore[union-attr] elif picked.n_blocks == 1: self_()._picked_cell = picked[0] # type: ignore[union-attr] else: self_()._picked_cell = picked # type: ignore[union-attr] finalize(self_()._picked_cell) # type: ignore[union-attr] self.enable_rectangle_picking( callback=through_pick_callback, show_message=show_message, font_size=font_size, show_frustum=show_frustum, start=start, style=style, color=color, ) @_deprecate_positional_args(allowed=['callback']) def enable_rectangle_visible_picking( # noqa: PLR0917 self, callback=None, show=True, # noqa: FBT002 style='wireframe', line_width=5, color='pink', show_message=True, # noqa: FBT002 font_size=18, start=False, # noqa: FBT002 show_frustum=False, # noqa: FBT002 **kwargs, ): """Enable rectangle based cell picking on visible surfaces. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The picked cells is the only passed argument. show : bool, default: True Show the selection interactively. style : str, default: "wireframe" Visualization style of the selection frustum. One of the following: ``style='surface'``, ``style='wireframe'``, or ``style='points'``. line_width : float, default: 5.0 Thickness of selected mesh edges. color : ColorLike, default: "pink" The color of the selected frustum when shown. show_message : bool | str, default: True Show the message about how to use the cell picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. start : bool, default: True Automatically start the cell selection tool. show_frustum : bool, default: False Show the frustum in the scene. **kwargs : dict, optional All remaining keyword arguments are used to control how the selection frustum is interactively displayed. """ self_ = weakref.ref(self) def finalize(picked): if picked is None: # Indicates invalid pick with self_().iren.poked_subplot(): # type: ignore[union-attr] self_()._clear_picking_representations() # type: ignore[union-attr] return if show: # Use try in case selection is empty with self_().iren.poked_subplot(): # type: ignore[union-attr] _kwargs = kwargs.copy() self_().add_mesh( # type: ignore[union-attr] picked, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['visible']), style=style, color=color, line_width=line_width, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callback is not None: _poked_context_callback(self_(), callback, picked) def visible_pick_callback(selection): picked = pyvista.MultiBlock() renderer = self_().iren.get_poked_renderer() # type: ignore[union-attr] x0, y0, x1, y1 = renderer.get_pick_position() # x0, y0, x1, y1 = selection.viewport if x0 >= 0: # initial pick position is (-1, -1, -1, -1) selector = _vtk.vtkOpenGLHardwareSelector() selector.SetFieldAssociation(_vtk.vtkDataObject.FIELD_ASSOCIATION_CELLS) selector.SetRenderer(renderer) selector.SetArea(x0, y0, x1, y1) selection = selector.Select() for node in range(selection.GetNumberOfNodes()): selection_node = selection.GetNode(node) if selection_node is None: # pragma: no cover # No selection continue cids = pyvista.convert_array(selection_node.GetSelectionList()) actor = selection_node.GetProperties().Get(_vtk.vtkSelectionNode.PROP()) # TODO: this is too hacky - find better way to avoid non-dataset actors if not actor.GetMapper() or not hasattr( actor.GetProperty(), 'GetRepresentation', ): continue # if not a surface if actor.GetProperty().GetRepresentation() != 2: # pragma: no cover warnings.warn( 'Display representations other than `surface` will result ' 'in incorrect results.', ) smesh = pyvista.wrap(_mapper_get_data_set_input(actor.GetMapper())) smesh = smesh.copy() smesh['original_cell_ids'] = np.arange(smesh.n_cells) tri_smesh = smesh.extract_surface().triangulate() cids_to_get = tri_smesh.extract_cells(cids)['original_cell_ids'] picked.append(smesh.extract_cells(cids_to_get)) # memory leak issues on vtk==9.0.20210612.dev0 # See: https://gitlab.kitware.com/vtk/vtk/-/issues/18239#note_973826 selection.UnRegister(selection) if len(picked) == 0 or picked.combine().n_cells < 1: self_()._picked_cell = None # type: ignore[union-attr] elif len(picked) == 1: self_()._picked_cell = picked[0] # type: ignore[union-attr] else: self_()._picked_cell = picked # type: ignore[union-attr] finalize(self_()._picked_cell) # type: ignore[union-attr] self.enable_rectangle_picking( callback=visible_pick_callback, show_message=show_message, font_size=font_size, start=start, show_frustum=show_frustum, style=style, color=color, ) @_deprecate_positional_args(allowed=['callback']) def enable_cell_picking( # noqa: PLR0917 self, callback=None, through=True, # noqa: FBT002 show=True, # noqa: FBT002 show_message=True, # noqa: FBT002 style='wireframe', line_width=5, color='pink', font_size=18, start=False, # noqa: FBT002 show_frustum=False, # noqa: FBT002 **kwargs, ): """Enable picking of cells with a rectangle selection tool. Press ``"r"`` to enable rectangle based selection. Press ``"r"`` again to turn it off. Selection will be saved to ``self.picked_cells``. All meshes in the scene are available for picking by default. If you would like to only pick a single mesh in the scene, use the ``pickable=False`` argument when adding the other meshes to the scene. When multiple meshes are being picked, the picked cells in ``self.picked_cells`` will be a :class:`MultiBlock` dataset for each mesh's selection. Uses last input mesh for input by default. .. warning:: Visible cell picking (``through=False``) will only work if the mesh is displayed with a ``'surface'`` representation style (the default). Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The picked_cells are input as the first parameter to this callable. through : bool, default: True When ``True`` the picker will select all cells through the mesh(es). When ``False``, the picker will select only visible cells on the selected surface(s). show : bool, default: True Show the selection interactively. show_message : bool | str, default: True Show the message about how to use the cell picking tool. If this is a string, that will be the message shown. style : str, default: "wireframe" Visualization style of the selection. One of the following: ``style='surface'``, ``style='wireframe'``, or ``style='points'``. line_width : float, default: 5.0 Thickness of selected mesh edges. color : ColorLike, default: "pink" The color of the selected mesh when shown. font_size : int, default: 18 Sets the font size of the message. start : bool, default: True Automatically start the cell selection tool. show_frustum : bool, default: False Show the frustum in the scene. **kwargs : dict, optional All remaining keyword arguments are used to control how the selection is interactively displayed. Examples -------- Add a mesh and a cube to a plot and enable cell picking. >>> import pyvista as pv >>> mesh = pv.Sphere(center=(1, 0, 0)) >>> cube = pv.Cube() >>> pl = pv.Plotter() >>> _ = pl.add_mesh(mesh) >>> _ = pl.add_mesh(cube) >>> _ = pl.enable_cell_picking() """ if through: method = self.enable_rectangle_through_picking else: method = self.enable_rectangle_visible_picking method( callback=callback, show=show, show_message=show_message, style=style, line_width=line_width, color=color, font_size=font_size, start=start, show_frustum=show_frustum, **kwargs, ) @_deprecate_positional_args(allowed=['callback']) def enable_element_picking( # noqa: PLR0917 self, callback=None, mode='cell', show=True, # noqa: FBT002 show_message=True, # noqa: FBT002 font_size=18, tolerance=0.025, pickable_window=False, # noqa: FBT002 left_clicking=False, # noqa: FBT002 picker=PickerType.CELL, **kwargs, ): """Select individual elements on a mesh. Parameters ---------- callback : callable, optional When input, calls this callable after a selection is made. The ``mesh`` is input as the first parameter to this callable. mode : str | ElementType, default: "cell" The picking mode. Either ``"mesh"``, ``"cell"``, ``"face"``, ``"edge"``, or ``"point"``. show : bool, default: True Show the selection interactively. show_message : bool | str, default: True Show the message about how to use the mesh picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. tolerance : float, default: 0.025 Specify tolerance for performing pick operation. Tolerance is specified as fraction of rendering window size. Rendering window size is measured across diagonal. .. warning:: This is ignored with the ``'hardware'`` ``picker``. pickable_window : bool, default: False When ``True``, points in the 3D window are pickable. left_clicking : bool, default: False When ``True``, meshes can be picked by clicking the left mousebutton. .. note:: If enabled, left-clicking will **not** display the bounding box around the picked mesh. picker : str | PickerType, optional Choice of VTK picker class type: * ``'hardware'``: Uses :vtk:`vtkHardwarePicker` which is more performant for large geometries (default). * ``'cell'``: Uses :vtk:`vtkCellPicker`. * ``'point'``: Uses :vtk:`vtkPointPicker` which will snap to points on the surface of the mesh. * ``'volume'``: Uses :vtk:`vtkVolumePicker`. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. See Also -------- :ref:`element_picking_example` """ mode = ElementType.from_any(mode) self_ = weakref.ref(self) def _end_handler(picked): if callback: _poked_context_callback(self_(), callback, picked) if mode == ElementType.CELL: self._picked_cell = picked if show: if mode == ElementType.CELL: kwargs.setdefault('color', 'pink') elif mode == ElementType.EDGE: kwargs.setdefault('color', 'magenta') else: kwargs.setdefault('color', 'pink') if mode in [ElementType.CELL, ElementType.FACE]: picked = picked.extract_all_edges() with self.iren.poked_subplot(): # type: ignore[attr-defined] _kwargs = kwargs.copy() self.add_mesh( # type: ignore[attr-defined] picked, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['element']), pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), point_size=_kwargs.pop('point_size', 5), line_width=_kwargs.pop('line_width', 5), **_kwargs, ) handler = PointPickingElementHandler(mode=mode, callback=_end_handler) self.enable_surface_point_picking( callback=handler, show_message=show_message, font_size=font_size, show_point=False, tolerance=tolerance, pickable_window=pickable_window, left_clicking=left_clicking, picker=picker, use_picker=True, **kwargs, ) def enable_block_picking(self, callback=None, side='left'): """Enable composite block picking. Use this picker to return the index of a DataSet when using composite dataset like :class:`pyvista.MultiBlock` and pass it to a callback. Parameters ---------- callback : callable, optional When input, this picker calls this callable after a selection is made. The composite index is passed to ``callback`` as the first argument and the dataset as the second argument. side : str, default: "left" The mouse button to track (either ``'left'`` or ``'right'``). Also accepts ``'r'`` or ``'l'``. Notes ----- The picked block index can be accessed from :attr:`picked_block_index ` attribute. Examples -------- Enable block picking with a multiblock dataset. Left clicking will turn blocks blue while right picking will turn the block back to the default color. >>> import pyvista as pv >>> multiblock = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite(multiblock) >>> def turn_blue(index, dataset): ... mapper.block_attr[index].color = 'blue' >>> pl.enable_block_picking(callback=turn_blue, side='left') >>> def clear_color(index, dataset): ... mapper.block_attr[index].color = None >>> pl.enable_block_picking(callback=clear_color, side='right') >>> pl.show() """ # use a weak reference to enable garbage collection self_ = weakref.ref(self) sel_index = _vtk.vtkSelectionNode.COMPOSITE_INDEX() sel_prop = _vtk.vtkSelectionNode.PROP() def get_picked_block(*args, **kwargs): # numpydoc ignore=PR01 # noqa: ARG001 """Get the picked block and pass it to the user callback.""" x, y = self.mouse_position # type: ignore[attr-defined] loc = self_().iren.get_event_subplot_loc() # type: ignore[union-attr] index = self_().renderers.loc_to_index(loc) # type: ignore[union-attr] renderer = self_().renderers[index] # type: ignore[union-attr] selector = _vtk.vtkOpenGLHardwareSelector() selector.SetRenderer(renderer) selector.SetArea(x, y, x, y) selection = selector.Select() for ii in range(selection.GetNumberOfNodes()): node = selection.GetNode(ii) if node is None: # pragma: no cover continue node_prop = node.GetProperties() self._picked_block_index = node_prop.Get(sel_index) # Safely return the dataset as it's possible a non pyvista # mapper was added mapper = node_prop.Get(sel_prop).GetMapper() if isinstance(mapper, CompositePolyDataMapper): dataset = mapper.block_attr.get_block(self._picked_block_index) else: # pragma: no cover dataset = None if callable(callback): _poked_context_callback(self_(), callback, self._picked_block_index, dataset) self.track_click_position(callback=get_picked_block, viewport=True, side=side) # type: ignore[attr-defined] @abstract_class class PickingHelper(PickingMethods): """Internal container class to contain picking helper methods.""" def __init__(self, *args, **kwargs): """Initialize the picking methods.""" super().__init__(*args, **kwargs) self.picked_path = None self.picked_geodesic = None self.picked_horizon = None self._last_picked_idx: int | None = None @_deprecate_positional_args def fly_to_mouse_position(self, focus=False): # noqa: FBT002 """Focus on last stored mouse position.""" if self.mouse_position is None: # type: ignore[attr-defined] self.store_mouse_position() # type: ignore[attr-defined] click_point = self.pick_mouse_position() if focus: self.set_focus(click_point) # type: ignore[attr-defined] else: self.fly_to(click_point) # type: ignore[attr-defined] def enable_fly_to_right_click(self, callback=None): """Set the camera to track right click positions. A convenience method to track right click positions and fly to the picked point in the scene. The callback will be passed the point in 3D space. Parameters ---------- callback : callable Callback to call immediately after right clicking. """ self_ = weakref.ref(self) def _the_callback(*_): click_point = self.pick_mouse_position() self.fly_to(click_point) # type: ignore[attr-defined] if callable(callback): _poked_context_callback(self_(), callback, click_point) self.track_click_position(callback=_the_callback, side='right') # type: ignore[attr-defined] @_deprecate_positional_args(allowed=['callback']) def enable_path_picking( # noqa: PLR0917 self, callback=None, show_message=True, # noqa: FBT002 font_size=18, color='pink', point_size=10, line_width=5, show_path=True, # noqa: FBT002 tolerance=0.025, **kwargs, ): """Enable picking at paths. This is a convenience method for :func:`enable_point_picking ` to keep track of the picked points and create a line using those points. The line is saved to the ``.picked_path`` attribute of this plotter Parameters ---------- callback : callable, optional When given, calls this callable after a pick is made. The entire picked path is passed as the only parameter to this callable. show_message : bool | str, default: True Show the message about how to use the point picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the size of the message. color : ColorLike, default: "pink" The color of the selected mesh when shown. point_size : int, default: 10 Size of picked points if ``show_path`` is ``True``. line_width : float, default: 5.0 Thickness of path representation if ``show_path`` is ``True``. show_path : bool, default: True Show the picked path interactively. tolerance : float, default: 0.025 Specify tolerance for performing pick operation. Tolerance is specified as fraction of rendering window size. Rendering window size is measured across diagonal. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. """ self_ = weakref.ref(self) kwargs.setdefault('pickable', False) def make_line_cells(n_points): cells = np.arange(0, n_points, dtype=np.int_) return np.insert(cells, 0, n_points) the_points = [] def _the_callback(picked_point, picker): if picker.GetDataSet() is None: return the_points.append(picked_point) self.picked_path = pyvista.PolyData(np.array(the_points)) self.picked_path.lines = make_line_cells(len(the_points)) if show_path: with self.iren.poked_subplot(): # type: ignore[attr-defined] _kwargs = kwargs.copy() self.add_mesh( # type: ignore[attr-defined] self.picked_path, color=color, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['path']), line_width=line_width, point_size=point_size, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callable(callback): _poked_context_callback(self_(), callback, self.picked_path) def _clear_path_event_watcher(): del the_points[:] with self.iren.poked_subplot(): # type: ignore[attr-defined] self._clear_picking_representations() self.add_key_event('c', _clear_path_event_watcher) # type: ignore[attr-defined] if show_message is True: show_message = 'Press P to pick under the mouse\nPress C to clear' self.enable_surface_point_picking( callback=_the_callback, use_picker=True, font_size=font_size, show_message=show_message, show_point=False, tolerance=tolerance, clear_on_no_selection=False, ) @_deprecate_positional_args(allowed=['callback']) def enable_geodesic_picking( # noqa: PLR0917 self, callback=None, show_message=True, # noqa: FBT002 font_size=18, color='pink', point_size=10, line_width=5, tolerance=0.025, show_path=True, # noqa: FBT002 keep_order=True, # noqa: FBT002 **kwargs, ): """Enable picking at geodesic paths. This is a convenience method for ``enable_point_picking`` to keep track of the picked points and create a geodesic path using those points. The geodesic path is saved to the ``.picked_geodesic`` attribute of this plotter. Parameters ---------- callback : callable, optional When given, calls this callable after a pick is made. The entire picked, geodesic path is passed as the only parameter to this callable. show_message : bool | str, default: True Show the message about how to use the point picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the size of the message. color : ColorLike, default: "pink" The color of the selected mesh when shown. point_size : int, default: 10 Size of picked points if ``show_path`` is ``True``. line_width : float, default: 5.0 Thickness of path representation if ``show_path`` is ``True``. tolerance : float, default: 0.025 Specify tolerance for performing pick operation. Tolerance is specified as fraction of rendering window size. Rendering window size is measured across diagonal. show_path : bool, default: True Show the picked path interactively. keep_order : bool, default: True If ``True``, the created geodesic path is a single ordered and cleaned line from the first point to the last. .. note:: In older versions there were apparent discontinuities in the resulting path due to the behavior of the underlying VTK filter which corresponds to ``keep_order=False``. .. versionadded:: 0.32.0 **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. """ self_ = weakref.ref(self) kwargs.setdefault('pickable', False) self.picked_geodesic = pyvista.PolyData() def _the_callback(picked_point, picker): if picker.GetDataSet() is None: return mesh = pyvista.wrap(picker.GetDataSet()) idx = mesh.find_closest_point(picked_point) point = mesh.points[idx] if self._last_picked_idx is None: self.picked_geodesic = pyvista.PolyData(point) self.picked_geodesic['vtkOriginalPointIds'] = [idx] else: surface = mesh.extract_surface().triangulate() locator = _vtk.vtkPointLocator() locator.SetDataSet(surface) locator.BuildLocator() start_idx = locator.FindClosestPoint(mesh.points[self._last_picked_idx]) end_idx = locator.FindClosestPoint(point) self.picked_geodesic += surface.geodesic(start_idx, end_idx, keep_order=keep_order) if keep_order: # it makes sense to remove adjacent duplicate points self.picked_geodesic.clean( inplace=True, lines_to_points=False, polys_to_lines=False, strips_to_polys=False, ) self._last_picked_idx = idx if show_path: with self.iren.poked_subplot(): # type: ignore[attr-defined] _kwargs = kwargs.copy() self.add_mesh( # type: ignore[attr-defined] self.picked_geodesic, color=color, name=_kwargs.pop('name', PICKED_REPRESENTATION_NAMES['path']), line_width=line_width, point_size=point_size, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callable(callback): _poked_context_callback(self_(), callback, self.picked_geodesic) def _clear_g_path_event_watcher(): self.picked_geodesic = pyvista.PolyData() with self.iren.poked_subplot(): # type: ignore[attr-defined] self._clear_picking_representations() self._last_picked_idx = None self.add_key_event('c', _clear_g_path_event_watcher) # type: ignore[attr-defined] if show_message is True: show_message = 'Press P to pick under the mouse\nPress C to clear' self.enable_surface_point_picking( callback=_the_callback, use_picker=True, font_size=font_size, show_message=show_message, tolerance=tolerance, show_point=False, clear_on_no_selection=False, ) @_deprecate_positional_args(allowed=['callback']) def enable_horizon_picking( # noqa: PLR0917 self, callback=None, normal=(0.0, 0.0, 1.0), width=None, show_message=True, # noqa: FBT002 font_size=18, color='pink', point_size=10, line_width=5, show_path=True, # noqa: FBT002 opacity=0.75, show_horizon=True, # noqa: FBT002 **kwargs, ): """Enable horizon picking. Helper for the ``enable_path_picking`` method to also show a ribbon surface along the picked path. Ribbon is saved under ``.picked_horizon``. Parameters ---------- callback : callable, optional When given, calls this callable after a pick is made. The entire picked path is passed as the only parameter to this callable. normal : sequence[float], default: (0.0, 0.0, 1.0) The normal to the horizon surface's projection plane. width : float, optional The width of the horizon surface. Default behaviour will dynamically change the surface width depending on its length. show_message : bool | str, default: True Show the message about how to use the horizon picking tool. If this is a string, that will be the message shown. font_size : int, default: 18 Sets the font size of the message. color : ColorLike, default: "pink" The color of the horizon surface if shown. point_size : int, default: 10 Size of picked points if ``show_horizon`` is ``True``. line_width : float, default: 5.0 Thickness of path representation if ``show_horizon`` is ``True``. show_path : bool, default: True Show the picked path that the horizon is built from interactively. opacity : float, default: 0.75 The opacity of the horizon surface if shown. show_horizon : bool, default: True Show the picked horizon surface interactively. **kwargs : dict, optional All remaining keyword arguments are used to control how the picked path is interactively displayed. """ self_ = weakref.ref(self) def _clear_horizon_event_watcher(): self.picked_horizon = pyvista.PolyData() with self.iren.poked_subplot(): # type: ignore[attr-defined] self._clear_picking_representations() self.add_key_event('c', _clear_horizon_event_watcher) # type: ignore[attr-defined] def _the_callback(path): if path.n_points < 2: _clear_horizon_event_watcher() return self.picked_horizon = path.ribbon(normal=normal, width=width) if show_horizon: with self.iren.poked_subplot(): # type: ignore[attr-defined] _kwargs = kwargs.copy() self.add_mesh( # type: ignore[attr-defined] self.picked_horizon, name=_kwargs.get('name', PICKED_REPRESENTATION_NAMES['horizon']), color=color, opacity=opacity, pickable=_kwargs.pop('pickable', False), reset_camera=_kwargs.pop('reset_camera', False), **_kwargs, ) if callable(callback): _poked_context_callback(self_(), callback, path) self.enable_path_picking( callback=_the_callback, show_message=show_message, font_size=font_size, color=color, point_size=point_size, line_width=line_width, show_path=show_path, **kwargs, )