"""These are private methods we keep out of plotting.py to simplify the module.""" from __future__ import annotations from typing import TYPE_CHECKING import warnings 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.misc import assert_empty_kwargs from .colors import Color from .opts import InterpolationType from .tools import opacity_transfer_function if TYPE_CHECKING: from pyvista.core._typing_core import NumpyArray @_deprecate_positional_args def prepare_smooth_shading( # noqa: PLR0917 mesh: pyvista.DataSet, scalars, texture, split_sharp_edges, feature_angle, preference ) -> tuple[pyvista.PolyData, NumpyArray[float]]: """Prepare a dataset for smooth shading. VTK requires datasets with Phong shading to have active normals. This requires extracting the external surfaces from non-polydata datasets and computing the point normals. Parameters ---------- mesh : pyvista.DataSet Dataset to prepare smooth shading for. scalars : sequence Sequence of scalars. texture : pyvista.Texture or np.ndarray, optional A texture to apply to the mesh. split_sharp_edges : bool Split sharp edges exceeding 30 degrees when plotting with smooth shading. Control the angle with the optional keyword argument ``feature_angle``. By default this is ``False``. Note that enabling this will create a copy of the input mesh within the plotter. See :ref:`shading_example`. feature_angle : float Angle to consider an edge a sharp edge. preference : str If the number of points is identical to the number of cells. Either ``'point'`` or ``'cell'``. Returns ------- pyvista.PolyData Always a surface as we need to compute point normals. """ is_polydata = isinstance(mesh, pyvista.PolyData) indices_array = None has_scalars = scalars is not None use_points = False if has_scalars: if not isinstance(scalars, np.ndarray): scalars = np.array(scalars) if scalars.shape[0] == mesh.n_points and scalars.shape[0] == mesh.n_cells: use_points = preference == 'point' else: use_points = scalars.shape[0] == mesh.n_points # extract surface if not already a surface if not is_polydata: mesh = mesh.extract_surface( pass_pointid=use_points or texture is not None, pass_cellid=not use_points, ) indices_array = 'vtkOriginalPointIds' if use_points else 'vtkOriginalCellIds' try: if split_sharp_edges: mesh = mesh.compute_normals( cell_normals=False, split_vertices=True, feature_angle=feature_angle, ) if is_polydata: if has_scalars and use_points: # we must track the original IDs with our own array from compute_normals indices_array = 'pyvistaOriginalPointIds' elif mesh.point_data.active_normals is None: mesh.compute_normals(cell_normals=False, inplace=True) except TypeError as e: if 'Normals cannot be computed' in repr(e): pass else: raise if has_scalars and indices_array is not None: ind = mesh[indices_array] scalars = np.asarray(scalars)[ind] return mesh, scalars # type: ignore[return-value] @_deprecate_positional_args def process_opacity(mesh, opacity, preference, n_colors, scalars, use_transparency): # noqa: PLR0917 """Process opacity. This function accepts an opacity string or array and always returns an array that can be applied to a dataset for plotting. Parameters ---------- mesh : pyvista.DataSet Dataset to process the opacity for. opacity : str, sequence String or array. If string, can be a ``str`` name of a predefined mapping such as ``'linear'``, ``'geom'``, ``'sigmoid'``, ``'sigmoid3-10'``, or the key of a cell or point data array. preference : str When ``mesh.n_points == mesh.n_cells``, this parameter sets how the scalars will be mapped to the mesh. If ``'point'``, causes the scalars will be associated with the mesh points. Can be either ``'point'`` or ``'cell'``. n_colors : int Number of colors to use when displaying the opacity. scalars : numpy.ndarray Dataset scalars. use_transparency : bool Invert the opacity mappings and make the values correspond to transparency. Returns ------- custom_opac : bool If using custom opacity. opacity : numpy.ndarray Array containing the opacity. """ custom_opac = False if isinstance(opacity, str): try: # Get array from mesh opacity = get_array(mesh, opacity, preference=preference, err=True) if np.any(opacity > 1): warnings.warn('Opacity scalars contain values over 1') if np.any(opacity < 0): warnings.warn('Opacity scalars contain values less than 0') custom_opac = True except KeyError: # Or get opacity transfer function (e.g. "linear") opacity = opacity_transfer_function(opacity, n_colors) else: if scalars.shape[0] != opacity.shape[0]: msg = 'Opacity array and scalars array must have the same number of elements.' raise ValueError(msg) elif isinstance(opacity, (np.ndarray, list, tuple)): opacity = np.asanyarray(opacity) if opacity.shape[0] in [mesh.n_cells, mesh.n_points]: # User could pass an array of opacities for every point/cell custom_opac = True else: opacity = opacity_transfer_function(opacity, n_colors) if use_transparency: if np.max(opacity) <= 1.0: opacity = 1 - opacity elif isinstance(opacity, np.ndarray): opacity = 255 - opacity return custom_opac, opacity def _common_arg_parser( *, dataset, theme, n_colors, scalar_bar_args, split_sharp_edges, show_scalar_bar, render_points_as_spheres, smooth_shading, pbr, clim, cmap, culling, name, nan_color, nan_opacity, texture, rgb, style, **kwargs, ): """Parse arguments in common between add_volume, composite, and mesh.""" # supported aliases clim = kwargs.pop('rng', clim) cmap = kwargs.pop('colormap', cmap) culling = kwargs.pop('backface_culling', culling) rgb = kwargs.pop('rgba', rgb) vertex_color = kwargs.pop('vertex_color', theme.edge_color) vertex_style = kwargs.pop('vertex_style', 'points') vertex_opacity = kwargs.pop('vertex_opacity', 1.0) # Support aliases for 'back', 'front', or 'none'. Consider deprecating if culling is False: culling = 'none' elif culling in ['b', 'backface', True]: culling = 'back' elif culling in ['f', 'frontface']: culling = 'front' if show_scalar_bar is None: # use theme unless plotting RGB _default = theme.show_scalar_bar or scalar_bar_args show_scalar_bar = False if rgb else _default # Avoid mutating input scalar_bar_args = {'n_colors': n_colors} if scalar_bar_args is None else scalar_bar_args.copy() # theme based parameters if split_sharp_edges is None: split_sharp_edges = theme.split_sharp_edges feature_angle = kwargs.pop('feature_angle', theme.sharp_edges_feature_angle) if render_points_as_spheres is None: if style == 'points_gaussian': render_points_as_spheres = False else: render_points_as_spheres = theme.render_points_as_spheres if smooth_shading is None: smooth_shading = True if pbr else theme.smooth_shading if name is None: name = f'{type(dataset).__name__}({dataset.memory_address})' remove_existing_actor = False else: # check if this actor already exists remove_existing_actor = True nan_color = Color(nan_color, opacity=nan_opacity, default_color=theme.nan_color) if texture is False: texture = None # allow directly specifying interpolation (potential future feature) if 'interpolation' in kwargs: interpolation = kwargs.pop('interpolation') # pragma: no cover: elif pbr: interpolation = InterpolationType.PBR elif smooth_shading: interpolation = InterpolationType.PHONG else: interpolation = theme.lighting_params.interpolation if 'scalar' in kwargs: msg = '`scalar` is an invalid keyword argument. Perhaps you mean `scalars` with an s?' raise TypeError(msg) assert_empty_kwargs(**kwargs) return ( scalar_bar_args, split_sharp_edges, show_scalar_bar, feature_angle, render_points_as_spheres, smooth_shading, clim, cmap, culling, name, nan_color, texture, rgb, interpolation, remove_existing_actor, vertex_color, vertex_style, vertex_opacity, )