"""An internal module for wrapping the use of mappers.""" from __future__ import annotations import sys from typing import Optional from typing import cast import numpy as np import pyvista from pyvista._deprecate_positional_args import _deprecate_positional_args from pyvista.core._typing_core import BoundsTuple from pyvista.core.utilities.arrays import FieldAssociation from pyvista.core.utilities.arrays import convert_array from pyvista.core.utilities.arrays import convert_string_array from pyvista.core.utilities.arrays import raise_not_matching from pyvista.core.utilities.helpers import wrap from pyvista.core.utilities.misc import _BoundsSizeMixin from pyvista.core.utilities.misc import _NoNewAttrMixin from pyvista.core.utilities.misc import abstract_class from . import _vtk from .colors import Color from .colors import get_cmap_safe from .lookup_table import LookupTable from .tools import normalize from .utilities.algorithms import set_algorithm_input @abstract_class class _BaseMapper( _NoNewAttrMixin, _BoundsSizeMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkAbstractMapper ): """Base Mapper with methods common to other mappers.""" def __init__(self, theme=None, **kwargs) -> None: self._theme = pyvista.themes.Theme() if theme is None: # copy global theme to ensure local property theme is fixed # after creation. self._theme.load_theme(pyvista.global_theme) else: self._theme.load_theme(theme) self.lookup_table = LookupTable() self.interpolate_before_map = kwargs.get( 'interpolate_before_map', self._theme.interpolate_before_map, ) @property def bounds(self) -> BoundsTuple: # numpydoc ignore=RT01 """Return the bounds of this mapper. Examples -------- >>> import pyvista as pv >>> mapper = pv.DataSetMapper(dataset=pv.Cube()) >>> mapper.bounds BoundsTuple(x_min = -0.5, x_max = 0.5, y_min = -0.5, y_max = 0.5, z_min = -0.5, z_max = 0.5) """ return BoundsTuple(*self.GetBounds()) @property def center(self) -> tuple[float, float, float]: """Return the center of mapper. Returns ------- tuple[float, float, float] Center of the active renderer. """ return self.GetCenter() def copy(self) -> _BaseMapper: """Create a copy of this mapper. Returns ------- pyvista.DataSetMapper A copy of this dataset mapper. Examples -------- >>> import pyvista as pv >>> mapper = pv.DataSetMapper(dataset=pv.Cube()) >>> mapper_copy = mapper.copy() """ new_mapper = type(self)(theme=self._theme) # even though this uses ShallowCopy, the new mapper no longer retains # any connection with the original new_mapper.ShallowCopy(self) if hasattr(self, 'dataset'): new_mapper.dataset = self.dataset return new_mapper @property def scalar_range(self) -> tuple[float, float]: # numpydoc ignore=RT01 """Return or set the scalar range. Examples -------- Return the scalar range of a mapper. >>> import pyvista as pv >>> mesh = pv.Sphere() >>> pl = pv.Plotter() >>> actor = pl.add_mesh(mesh, scalars=mesh.points[:, 2]) >>> actor.mapper.scalar_range (-0.5, 0.5) >>> pl.close() Return the scalar range of a composite dataset. In this example it's set to its default value of ``(0.0, 1.0)``. >>> import pyvista as pv >>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite(dataset) >>> mapper.scalar_range (0.0, 1.0) >>> pl.close() """ return self.GetScalarRange() @scalar_range.setter def scalar_range(self, clim) -> None: self.SetScalarRange(*clim) @property def lookup_table(self) -> pyvista.LookupTable: # numpydoc ignore=RT01 """Return or set the lookup table. Examples -------- Return the lookup table of a dataset mapper. >>> import pyvista as pv >>> mesh = pv.Sphere() >>> pl = pv.Plotter() >>> actor = pl.add_mesh(mesh, scalars=mesh.points[:, 2], cmap='bwr') >>> actor.mapper.lookup_table LookupTable (...) Table Range: (-0.5, 0.5) N Values: 256 Above Range Color: None Below Range Color: None NAN Color: Color(name='darkgray', hex='#a9a9a9ff', opacity=255) Log Scale: False Color Map: "bwr" Return the lookup table of a composite dataset mapper. >>> import pyvista as pv >>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite(dataset) >>> mapper.lookup_table # doctest:+SKIP """ return self.GetLookupTable() @lookup_table.setter def lookup_table(self, table) -> None: self.SetLookupTable(table) @property def color_mode(self) -> str: # numpydoc ignore=RT01 """Return or set the color mode. Either ``'direct'``, or ``'map'``. * ``'direct'`` - All integer types are treated as colors with values in the range 0-255 and floating types are treated as colors with values in the range 0.0-1.0 * ``'map'`` - All scalar data will be mapped through the lookup table. """ mode = self.GetColorModeAsString().lower() if mode == 'mapscalars': return 'map' return 'direct' @color_mode.setter def color_mode(self, value: str): if value == 'direct': self.SetColorModeToDirectScalars() elif value == 'map': self.SetColorModeToMapScalars() else: msg = 'Color mode must be either "default", "direct" or "map"' raise ValueError(msg) @property def interpolate_before_map(self) -> bool | None: # numpydoc ignore=RT01 """Return or set the interpolation of scalars before mapping. Enabling makes for a smoother scalars display. When ``False``, OpenGL will interpolate the mapped colors which can result in showing colors that are not present in the color map. Examples -------- Disable interpolation before mapping. >>> import pyvista as pv >>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> dataset[0].point_data['data'] = dataset[0].points[:, 2] >>> dataset[1].point_data['data'] = dataset[1].points[:, 2] >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite( ... dataset, ... show_scalar_bar=False, ... n_colors=3, ... cmap='bwr', ... ) >>> mapper.interpolate_before_map = False >>> pl.show() Enable interpolation before mapping. >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite( ... dataset, ... show_scalar_bar=False, ... n_colors=3, ... cmap='bwr', ... ) >>> mapper.interpolate_before_map = True >>> pl.show() See :ref:`interpolate_before_map_example` for additional explanation regarding this attribute. """ return bool(self.GetInterpolateScalarsBeforeMapping()) @interpolate_before_map.setter def interpolate_before_map(self, value: bool) -> None: self.SetInterpolateScalarsBeforeMapping(value) @property def array_name(self) -> str: # numpydoc ignore=RT01 """Return or set the array name or number and component to color by. Examples -------- Show the name of the active scalars in the mapper. >>> import pyvista as pv >>> mesh = pv.Sphere() >>> mesh['my_scalars'] = mesh.points[:, 2] >>> pl = pv.Plotter() >>> actor = pl.add_mesh(mesh, scalars='my_scalars') >>> actor.mapper.array_name 'my_scalars' >>> pl.close() """ return self.GetArrayName() @array_name.setter def array_name(self, name: str) -> None: """Return or set the array name or number and component to color by.""" self.SetArrayName(name) @property def scalar_map_mode(self) -> str: # numpydoc ignore=RT01 """Return or set the scalar map mode. Examples -------- Show that the scalar map mode is set to ``'point'`` when setting the active scalars to point data. >>> import pyvista as pv >>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> dataset[0].point_data['data'] = dataset[0].points[:, 2] >>> dataset[1].point_data['data'] = dataset[1].points[:, 2] >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite( ... dataset, scalars='data', show_scalar_bar=False ... ) >>> mapper.scalar_map_mode 'point' >>> pl.close() """ # map vtk strings to more sensible strings vtk_to_pv = { 'Default': 'default', 'UsePointData': 'point', 'UseCellData': 'cell', 'UsePointFieldData': 'point_field', 'UseCellFieldData': 'cell_field', 'UseFieldData': 'field', } return vtk_to_pv[self.GetScalarModeAsString()] @scalar_map_mode.setter def scalar_map_mode(self, scalar_mode: str | FieldAssociation): if isinstance(scalar_mode, FieldAssociation): scalar_mode = scalar_mode.name scalar_mode = scalar_mode.lower() if scalar_mode == 'default': self.SetScalarModeToDefault() elif scalar_mode == 'point': self.SetScalarModeToUsePointData() elif scalar_mode == 'cell': self.SetScalarModeToUseCellData() elif scalar_mode == 'point_field': self.SetScalarModeToUsePointFieldData() elif scalar_mode == 'cell_field': self.SetScalarModeToUseCellFieldData() elif scalar_mode == 'field': self.SetScalarModeToUseFieldData() else: msg = ( f'Invalid `scalar_map_mode` "{scalar_mode}". Should be either ' '"default", "point", "cell", "point_field", "cell_field" or "field".' ) raise ValueError(msg) @property def scalar_visibility(self) -> bool: # numpydoc ignore=RT01 """Return or set the scalar visibility. Examples -------- Show that scalar visibility is ``False``. >>> import pyvista as pv >>> mesh = pv.Sphere() >>> pl = pv.Plotter() >>> actor = pl.add_mesh(mesh) >>> actor.mapper.scalar_visibility False >>> pl.close() Show that scalar visibility is ``True``. >>> import pyvista as pv >>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))]) >>> dataset[0].point_data['data'] = dataset[0].points[:, 2] >>> dataset[1].point_data['data'] = dataset[1].points[:, 2] >>> pl = pv.Plotter() >>> actor, mapper = pl.add_composite(dataset, scalars='data') >>> mapper.scalar_visibility True >>> pl.close() """ return bool(self.GetScalarVisibility()) @scalar_visibility.setter def scalar_visibility(self, value: bool) -> None: self.SetScalarVisibility(value) def update(self) -> None: """Update this mapper.""" self.Update() class _DataSetMapper(_BaseMapper): """Base wrapper for :vtk:`vtkDataSetMapper`. Parameters ---------- dataset : pyvista.DataSet, optional Dataset to assign to this mapper. theme : pyvista.plotting.themes.Theme, optional Plot-specific theme. """ _cmap = None def __init__( self, dataset: pyvista.DataSet | None = None, theme: pyvista.themes.Theme | None = None, ) -> None: """Initialize this class.""" super().__init__(theme=theme) if dataset is not None: self.dataset = dataset @property def dataset(self) -> pyvista.core.dataset.DataSet | None: # numpydoc ignore=RT01 """Return or set the dataset assigned to this mapper.""" return cast('Optional[pyvista.DataSet]', wrap(_mapper_get_data_set_input(self))) @dataset.setter def dataset( self, obj: pyvista.core.dataset.DataSet | _vtk.vtkAlgorithm | _vtk.vtkAlgorithmOutput, ) -> None: set_algorithm_input(self, obj) def as_rgba(self) -> None: """Convert the active scalars to RGBA. This method is used to convert the active scalars to a fixed RGBA array and is used for certain mappers that do not support the "map" color mode. """ if self.color_mode == 'direct': return if self.dataset is not None: self.dataset.point_data.pop('__rgba__', None) self._configure_scalars_mode( scalars=self.lookup_table(self.dataset.active_scalars), scalars_name='__rgba__', preference=self.scalar_map_mode, direct_scalars_color_mode=True, ) def _configure_scalars_mode( self, *, scalars, scalars_name, preference, direct_scalars_color_mode, ) -> None: """Configure scalar mode. Parameters ---------- scalars : numpy.ndarray Array of scalars to assign to the mapper. scalars_name : str If the name of this array exists, scalars is ignored. Otherwise, the scalars will be added to the existing dataset and this parameter is the name to assign the scalars. preference : str Either ``'point'`` or ``'cell'``. direct_scalars_color_mode : bool When ``True``, scalars are treated as RGB colors. When ``False``, scalars are mapped to the color table. """ if self.dataset is not None: if ( scalars.shape[0] == self.dataset.n_points and scalars.shape[0] == self.dataset.n_cells ): use_points = preference == 'point' use_cells = not use_points else: use_points = scalars.shape[0] == self.dataset.n_points use_cells = scalars.shape[0] == self.dataset.n_cells # Scalars interpolation approach if use_points: if ( scalars_name not in self.dataset.point_data or scalars_name == pyvista.DEFAULT_SCALARS_NAME ): self.dataset.point_data.set_array(scalars, scalars_name, deep_copy=False) self.dataset.active_scalars_name = scalars_name self.scalar_map_mode = 'point' elif use_cells: if ( scalars_name not in self.dataset.cell_data or scalars_name == pyvista.DEFAULT_SCALARS_NAME ): self.dataset.cell_data.set_array(scalars, scalars_name, deep_copy=False) self.dataset.active_scalars_name = scalars_name self.scalar_map_mode = 'cell' else: raise_not_matching(scalars, self.dataset) self.color_mode = 'direct' if direct_scalars_color_mode else 'map' @_deprecate_positional_args(allowed=['scalars', 'scalars_name']) def set_scalars( # noqa: PLR0917 self, scalars, scalars_name, n_colors=256, scalar_bar_args=None, rgb=None, component=None, preference='point', custom_opac: bool = False, # noqa: FBT001, FBT002 annotations=None, log_scale: bool = False, # noqa: FBT001, FBT002 nan_color=None, above_color=None, below_color=None, cmap=None, flip_scalars: bool = False, # noqa: FBT001, FBT002 opacity=None, categories: bool | int = False, # noqa: FBT001, FBT002 clim=None, ): """Set the scalars on this mapper. Parameters ---------- scalars : numpy.ndarray Array of scalars to assign to the mapper. scalars_name : str If the name of this array exists, scalars is ignored. Otherwise, the scalars will be added to the existing dataset and this parameter is the name to assign the scalars. n_colors : int, default: 256 Number of colors to use when displaying scalars. scalar_bar_args : dict, optional Dictionary of keyword arguments to pass when adding the scalar bar to the scene. For options, see :func:`pyvista.Plotter.add_scalar_bar`. rgb : bool, default: False If an 2 dimensional array is passed as the scalars, plot those values as RGB(A) colors. ``rgba`` is also an accepted alias for this. Opacity (the A) is optional. If a scalars array ending with ``"_rgba"`` is passed, the default becomes ``True``. This can be overridden by setting this parameter to ``False``. component : int, optional Set component of vector valued scalars to plot. Must be nonnegative, if supplied. If ``None``, the magnitude of the vector is plotted. preference : str, default: 'Point' When ``dataset.n_points == dataset.n_cells`` and setting scalars, this parameter sets how the scalars will be mapped to the mesh. Can be either ``'point'`` or ``'cell'``. custom_opac : bool, default: False Use custom opacity. annotations : dict, optional Pass a dictionary of annotations. Keys are the float values in the scalars range to annotate on the scalar bar and the values are the string annotations. log_scale : bool, default: False Use log scale when mapping data to colors. Scalars less than zero are mapped to the smallest representable positive float. nan_color : pyvista.ColorLike, optional The color to use for all ``NaN`` values in the plotted scalar array. above_color : pyvista.ColorLike, optional Solid color for values below the scalars range (``clim``). This will automatically set the scalar bar ``above_label`` to ``'above'``. below_color : pyvista.ColorLike, optional Solid color for values below the scalars range (``clim``). This will automatically set the scalar bar ``below_label`` to ``'below'``. cmap : str, list, or pyvista.LookupTable Name of the Matplotlib colormap to use when mapping the ``scalars``. See available Matplotlib colormaps. Only applicable for when displaying ``scalars``. ``colormap`` is also an accepted alias for this. If ``colorcet`` or ``cmocean`` are installed, their colormaps can be specified by name. You can also specify a list of colors to override an existing colormap with a custom one. For example, to create a three color colormap you might specify ``['green', 'red', 'blue']``. This parameter also accepts a :class:`pyvista.LookupTable`. If this is set, all parameters controlling the color map like ``n_colors`` will be ignored. flip_scalars : bool, default: False Flip direction of cmap. Most colormaps allow ``*_r`` suffix to do this as well. opacity : str or numpy.ndarray, optional Opacity mapping for the scalars array. A string can also be specified to map the scalars range to a predefined opacity transfer function (options include: 'linear', 'linear_r', 'geom', 'geom_r'). Or you can pass a custom made transfer function that is an array either ``n_colors`` in length or shorter. categories : bool, default: False If set to ``True``, then the number of unique values in the scalar array will be used as the ``n_colors`` argument. clim : Sequence, optional Color bar range for scalars. Defaults to minimum and maximum of scalars array. Example: ``(-1, 2)``. """ if scalar_bar_args is None: scalar_bar_args = {'n_colors': n_colors} if not isinstance(scalars, np.ndarray): scalars = np.asarray(scalars) # Set the array title for when it is added back to the mesh if custom_opac: scalars_name = '__custom_rgba' if not np.issubdtype(scalars.dtype, np.number) and not isinstance( cmap, pyvista.LookupTable, ): # we can rapidly handle bools if scalars.dtype == np.bool_: cats = np.array([b'False', b'True'], dtype='|S5') values = np.array([0, 1]) clim = [-0.5, 1.5] else: # If str array, digitize and annotate cats, scalars = np.unique(scalars.astype('|S'), return_inverse=True) values = np.unique(scalars) clim = [np.min(values) - 0.5, np.max(values) + 0.5] scalars_name = f'{scalars_name}-digitized' n_colors = len(cats) scalar_bar_args.setdefault('n_labels', 0) self.lookup_table.SetAnnotations(convert_array(values), convert_string_array(cats)) # Use only the real component if an array is complex if np.issubdtype(scalars.dtype, np.complexfloating): scalars = scalars.astype(float) scalars_name = f'{scalars_name}-real' if scalars.ndim != 1: if rgb: pass elif scalars.ndim == 2 and ( scalars.shape[0] == self.dataset.n_points # type: ignore[union-attr] or scalars.shape[0] == self.dataset.n_cells # type: ignore[union-attr] ): if not isinstance(component, (int, type(None))): msg = 'component must be either None or an integer' raise TypeError(msg) if component is None: scalars = np.linalg.norm(scalars.copy(), axis=1) scalars_name = f'{scalars_name}-normed' elif component < scalars.shape[1] and component >= 0: scalars = np.array(scalars[:, component]).copy() scalars_name = f'{scalars_name}-{component}' else: msg = ( 'Component must be nonnegative and less than the ' f'dimensionality of the scalars array: {scalars.shape[1]}' ) raise ValueError(msg) else: scalars = scalars.ravel() if scalars.dtype == np.bool_: scalars = scalars.astype(np.float64) # Set scalars range if clim is None: clim = [np.nanmin(scalars), np.nanmax(scalars)] elif isinstance(clim, (int, float)): clim = [-clim, clim] if log_scale and clim[0] <= 0: clim = [sys.float_info.min, clim[1]] if np.any(clim) and not rgb: self.scalar_range = clim[0], clim[1] if isinstance(cmap, pyvista.LookupTable): self.lookup_table = cmap self.scalar_range = self.lookup_table.scalar_range else: self.lookup_table.scalar_range = self.scalar_range # Set default map if cmap is None: cmap = pyvista.global_theme.cmap if self._theme is None else self._theme.cmap # have to add the attribute to pass it onward to some classes if isinstance(cmap, str): self._cmap = cmap if categories: if categories is True: n_colors = len(np.unique(scalars)) elif isinstance(categories, int): n_colors = categories self.lookup_table.apply_cmap(cmap, n_colors) # Set opactities if isinstance(opacity, np.ndarray) and not custom_opac: self.lookup_table.apply_opacity(opacity) if flip_scalars: self.lookup_table.values[:] = self.lookup_table.values[::-1] if custom_opac: # need to round the colors here since we're # directly displaying the colors hue = normalize(scalars, minimum=clim[0], maximum=clim[1]) scalars = np.round(hue * n_colors) / n_colors scalars = get_cmap_safe(cmap)(scalars) * 255 scalars[:, -1] *= opacity scalars = scalars.astype(np.uint8) # configure the lookup table if nan_color: self.lookup_table.nan_color = nan_color if above_color: self.lookup_table.above_range_color = above_color scalar_bar_args.setdefault('above_label', 'above') if below_color: self.lookup_table.below_range_color = below_color scalar_bar_args.setdefault('below_label', 'below') if isinstance(annotations, dict): self.lookup_table.annotations = annotations self.lookup_table.log_scale = log_scale self._configure_scalars_mode( scalars=scalars, scalars_name=scalars_name, preference=preference, direct_scalars_color_mode=rgb or custom_opac, ) if isinstance(self, PointGaussianMapper): self.as_rgba() @property def cmap(self) -> str | None: # numpydoc ignore=RT01 """Colormap assigned to this mapper. See :ref:`named_colormaps` for supported colormaps. """ return self._cmap @property def resolve(self) -> str: """Set or return the global flag to avoid z-buffer resolution. A global flag that controls whether the coincident topology (e.g., a line on top of a polygon) is shifted to avoid z-buffer resolution (and hence rendering problems). If not off, there are two methods to choose from. `polygon_offset` uses graphics systems calls to shift polygons, lines, and points from each other. `shift_zbuffer` is a legacy method that is used to remap the z-buffer to distinguish vertices, lines, and polygons, but does not always produce acceptable results. You should only use the polygon_offset method (or none) at this point. Returns ------- str Global flag to avoid z-buffer resolution. Must be either `off`, `polygon_offset` or `shift_zbuffer`. Examples -------- >>> import pyvista as pv >>> from pyvista import examples >>> mesh = examples.download_tri_quadratic_hexahedron() >>> surface_sep = mesh.separate_cells().extract_surface( ... nonlinear_subdivision=4 ... ) >>> edges = surface_sep.extract_feature_edges() >>> surface = mesh.extract_surface(nonlinear_subdivision=4) >>> plotter = pv.Plotter() >>> _ = plotter.add_mesh(surface, smooth_shading=True, split_sharp_edges=True) >>> actor = plotter.add_mesh(edges, color='k', line_width=3) >>> actor.mapper.resolve = 'polygon_offset' >>> plotter.show() """ vtk_to_pv = { _vtk.VTK_RESOLVE_OFF: 'off', _vtk.VTK_RESOLVE_POLYGON_OFFSET: 'polygon_offset', _vtk.VTK_RESOLVE_SHIFT_ZBUFFER: 'shift_zbuffer', } return vtk_to_pv[self.GetResolveCoincidentTopology()] @resolve.setter def resolve(self, resolve): if resolve == 'off': self.SetResolveCoincidentTopologyToOff() elif resolve == 'polygon_offset': self.SetResolveCoincidentTopologyToPolygonOffset() elif resolve == 'shift_zbuffer': self.SetResolveCoincidentTopologyToShiftZBuffer() else: msg = 'Resolve must be either "off", "polygon_offset" or "shift_zbuffer"' raise ValueError(msg) @_deprecate_positional_args(allowed=['opacity']) def set_custom_opacity( # noqa: PLR0917 self, opacity, color, n_colors, preference='point' ): """Set custom opacity. Parameters ---------- opacity : numpy.ndarray Opacity array to color the dataset. Array length must match either the number of points or cells. color : pyvista.ColorLike The color to use with the opacity array. n_colors : int Number of colors to use. preference : str, default: 'point' Either ``'point'`` or ``'cell'``. Used when the number of cells matches the number of points. """ # Create a custom RGBA array to supply our opacity to if opacity.size == self.dataset.n_points: # type: ignore[union-attr] rgba = np.empty((self.dataset.n_points, 4), np.uint8) # type: ignore[union-attr] elif opacity.size == self.dataset.n_cells: # type: ignore[union-attr] rgba = np.empty((self.dataset.n_cells, 4), np.uint8) # type: ignore[union-attr] else: # pragma: no cover msg = ( f'Opacity array size ({opacity.size}) does not equal ' f'the number of points ({self.dataset.n_points}) or the ' # type: ignore[union-attr] f'number of cells ({self.dataset.n_cells}).', # type: ignore[union-attr] ) raise ValueError(msg) default_color = ( self._theme.color if self._theme is not None else pyvista.global_theme.color ) rgba[:, :-1] = Color(color, default_color=default_color).int_rgb rgba[:, -1] = np.around(opacity * 255) self.color_mode = 'direct' self.lookup_table.n_values = n_colors self._configure_scalars_mode( scalars=rgba, scalars_name='', preference=preference, direct_scalars_color_mode=True ) def __repr__(self): """Representation of the mapper.""" mapper_attr = [ f'{type(self).__name__} ({hex(id(self))})', f' Scalar visibility: {self.scalar_visibility}', f' Scalar range: {self.scalar_range}', f' Interpolate before mapping: {self.interpolate_before_map}', f' Scalar map mode: {self.scalar_map_mode}', f' Color mode: {self.color_mode}', '', ] mapper_attr.append('Attached dataset:') mapper_attr.append(str(self.dataset)) return '\n'.join(mapper_attr) class DataSetMapper(_DataSetMapper, _vtk.vtkDataSetMapper): """Wrap :vtk:`vtkDataSetMapper`. Parameters ---------- dataset : pyvista.DataSet, optional Dataset to assign to this mapper. theme : pyvista.plotting.themes.Theme, optional Plot-specific theme. Examples -------- Create a mapper outside :class:`pyvista.Plotter` and assign it to an actor. >>> import pyvista as pv >>> mesh = pv.Cube() >>> mapper = pv.DataSetMapper(dataset=mesh) >>> actor = pv.Actor(mapper=mapper) >>> actor.plot() """ def __init__( self, dataset: pyvista.DataSet | None = None, theme: pyvista.themes.Theme | None = None, ) -> None: """Initialize this class.""" super().__init__(dataset=dataset, theme=theme) class PointGaussianMapper(_DataSetMapper, _vtk.vtkPointGaussianMapper): """Wrap :vtk:`vtkPointGaussianMapper`. Parameters ---------- theme : pyvista.Theme, optional The theme to be used. emissive : bool, optional Whether or not the point should appear emissive. Default is set by the theme's ``lighting_params.emissive``. scale_factor : float, default: 1.0 Scale factor applied to the point size. """ def __init__(self, theme=None, emissive=None, scale_factor=1.0) -> None: super().__init__(theme=theme) if emissive is None: emissive = self._theme.lighting_params.emissive self.emissive = emissive self.scale_factor = scale_factor @property def emissive(self) -> bool: # numpydoc ignore=RT01 """Set or return emissive. This treats points as emissive light sources. Two points that overlap will have their brightness combined. """ return bool(self.GetEmissive()) @emissive.setter def emissive(self, value: bool) -> None: self.SetEmissive(value) @property def scale_factor(self) -> float: # numpydoc ignore=RT01 """Set or return the scale factor. Ranges from 0 to 1. A value of 0 will cause the splats to be rendered as simple points. Defaults to 1.0. """ return self.GetScaleFactor() @scale_factor.setter def scale_factor(self, value: float) -> None: self.SetScaleFactor(value) @property def scale_array(self) -> str: # numpydoc ignore=RT01 """Set or return the name of the array used to scale the splats. Scalars used to scale the gaussian points. Accepts a string name of an array that is present on the mesh. Notes ----- Setting this automatically sets ``scale_factor = 1.0``. Examples -------- Plot spheres using `style='points_gaussian'` style and scale them by radius. >>> import numpy as np >>> import pyvista as pv >>> n_spheres = 1_000 >>> pos = np.random.random((n_spheres, 3)) >>> rad = np.random.random(n_spheres) * 0.01 >>> pdata = pv.PolyData(pos) >>> pdata['radius'] = rad >>> pl = pv.Plotter() >>> actor = pl.add_mesh( ... pdata, ... style='points_gaussian', ... emissive=False, ... render_points_as_spheres=True, ... ) >>> actor.mapper.scale_array = 'radius' >>> pl.show() """ return self.GetScaleArray() @scale_array.setter def scale_array(self, name: str): if not self.dataset: # pragma: no cover msg = 'Missing dataset.' raise RuntimeError(msg) if name not in self.dataset.point_data: available_arrays = ', '.join(self.dataset.point_data.keys()) msg = ( f'Point array "{name}" does not exist. ' f'Available point arrays are: {available_arrays}' ) raise KeyError(msg) self.scale_factor = 1.0 self.SetScaleArray(name) def use_circular_splat(self, opacity: float = 1.0) -> None: """Set the fragment shader code to create a circular splat. Parameters ---------- opacity : float, default: 1.0 Desired opacity between 0 and 1. Notes ----- This very close to ParaView's PointGaussianMapper, but uses opacity to modify the scale as the opacity cannot be set from the actor's property. """ self.SetSplatShaderCode( '//VTK::Color::Impl\n' 'float dist = dot(offsetVCVSOutput.xy,offsetVCVSOutput.xy);\n' 'if (dist > 1.0) {\n' ' discard;\n' '} else {\n' f' float scale = ({opacity} - dist);\n' ' ambientColor *= scale;\n' ' diffuseColor *= scale;\n' '}\n', ) # maintain consistency with the default style self.scale_factor *= 1.5 def use_default_splat(self) -> None: """Clear the fragment shader and use the default splat.""" self.SetSplatShaderCode(None) # type: ignore[arg-type] self.scale_factor /= 1.5 def __repr__(self): """Representation of the Gaussian mapper.""" mapper_attr = [ f'{type(self).__name__} ({hex(id(self))})', f' Scalar visibility: {self.scalar_visibility}', f' Scalar range: {self.scalar_range}', f' Emissive: {self.emissive}', f' Scale Factor: {self.scale_factor}', f' Using custom splat: {self.GetSplatShaderCode() is None}', '', ] mapper_attr.append('Attached dataset:') mapper_attr.append(str(self.dataset)) return '\n'.join(mapper_attr) @abstract_class class _BaseVolumeMapper(_BaseMapper): """Volume mapper class to override methods and attributes for to volume mappers.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self._lut = LookupTable() self._scalar_range = (0.0, 256.0) @property def interpolate_before_map(self) -> bool | None: # numpydoc ignore=RT01 """Interpolate before map is not supported with volume mappers.""" return None @interpolate_before_map.setter def interpolate_before_map(self, *args) -> None: pass @property def dataset(self): # numpydoc ignore=RT01 """Return or set the dataset assigned to this mapper.""" return wrap(_mapper_get_data_set_input(self)) @dataset.setter def dataset( self, obj: pyvista.core.dataset.DataSet | _vtk.vtkAlgorithm | _vtk.vtkAlgorithmOutput, ) -> None: set_algorithm_input(self, obj) @property def lookup_table(self): # numpydoc ignore=RT01 return self._lut @lookup_table.setter def lookup_table(self, lut) -> None: self._lut = lut @property def scalar_range(self) -> tuple[float, float]: # numpydoc ignore=RT01 """Return or set the scalar range.""" return self._scalar_range @scalar_range.setter def scalar_range(self, clim) -> None: if self.lookup_table is not None: self.lookup_table.SetRange(*clim) self._scalar_range = tuple(clim) @property def blend_mode(self) -> str: # numpydoc ignore=RT01 """Return or set the blend mode. One of the following: * ``"composite"`` * ``"maximum"`` * ``"minimum"`` * ``"average"`` * ``"additive"`` Also accepts integer values corresponding to :vtk:`vtkVolumeMapper.BlendModes`. For example :vtk:`vtkVolumeMapper.COMPOSITE_BLEND`. """ value = self.GetBlendMode() mode = { 0: 'composite', 1: 'maximum', 2: 'minimum', 3: 'average', 4: 'additive', }.get(value) if mode is None: # pragma: no cover msg = f'Unsupported blend mode return value {value}' raise NotImplementedError(msg) return mode @blend_mode.setter def blend_mode(self, value: str | int): if isinstance(value, int): self.SetBlendMode(value) elif isinstance(value, str): value = value.lower() if value in ['additive', 'add', 'sum']: self.SetBlendModeToAdditive() elif value in ['average', 'avg', 'average_intensity']: self.SetBlendModeToAverageIntensity() elif value in ['composite', 'comp']: self.SetBlendModeToComposite() elif value in ['maximum', 'max', 'maximum_intensity']: self.SetBlendModeToMaximumIntensity() elif value in ['minimum', 'min', 'minimum_intensity']: self.SetBlendModeToMinimumIntensity() else: msg = ( f'Blending mode {value!r} invalid. ' 'Please choose either "additive", ' '"composite", "minimum" or "maximum".' ) raise ValueError(msg) else: msg = f'`blend_mode` should be either an int or str, not `{type(value)}`' # type: ignore[unreachable] raise TypeError(msg) def __del__(self) -> None: if hasattr(self, '_lut'): del self._lut class FixedPointVolumeRayCastMapper(_BaseVolumeMapper, _vtk.vtkFixedPointVolumeRayCastMapper): """Wrap :vtk:`vtkFixedPointVolumeRayCastMapper`.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self.AutoAdjustSampleDistancesOff() class GPUVolumeRayCastMapper(_BaseVolumeMapper, _vtk.vtkGPUVolumeRayCastMapper): """Wrap :vtk:`vtkGPUVolumeRayCastMapper`.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self.AutoAdjustSampleDistancesOff() class OpenGLGPUVolumeRayCastMapper(_BaseVolumeMapper, _vtk.vtkOpenGLGPUVolumeRayCastMapper): """Wrap :vtk:`vtkOpenGLGPUVolumeRayCastMapper`.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self.AutoAdjustSampleDistancesOff() class SmartVolumeMapper(_BaseVolumeMapper, _vtk.vtkSmartVolumeMapper): """Wrap :vtk:`vtkSmartVolumeMapper`.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self.AutoAdjustSampleDistancesOff() self.InteractiveAdjustSampleDistancesOff() class UnstructuredGridVolumeRayCastMapper( _BaseVolumeMapper, _vtk.vtkUnstructuredGridVolumeRayCastMapper ): """Wrap :vtk:`vtkUnstructuredGridVolumeMapper`.""" def __init__(self, theme=None) -> None: """Initialize this class.""" super().__init__(theme=theme) self.AutoAdjustSampleDistancesOff() def _mapper_has_data_set_input(mapper): """Check if mapper has a data set input using the appropriate method. Some mappers use 'GetDataSetInput', others use 'GetInputAsDataSet'. This has been standardized to 'GetDataSetInput' in VTK >= 9.5. """ return hasattr(mapper, 'GetDataSetInput') or hasattr(mapper, 'GetInputAsDataSet') def _mapper_get_data_set_input(mapper): """Get data set input from mapper using the appropriate method. Some mappers use 'GetDataSetInput', others use 'GetInputAsDataSet'. This has been standardized to 'GetDataSetInput' in VTK >= 9.5. """ return ( mapper.GetDataSetInput() if hasattr(mapper, 'GetDataSetInput') else mapper.GetInputAsDataSet() )