This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,122 @@
"""Plotting routines."""
from __future__ import annotations
from pyvista import MAX_N_COLOR_BARS as MAX_N_COLOR_BARS
from pyvista._plot import plot as plot
from . import _vtk as _vtk
from ._property import Property as Property
from ._typing import Chart as Chart
from ._typing import ColorLike as ColorLike
from .actor import Actor as Actor
from .actor_properties import ActorProperties as ActorProperties
from .affine_widget import AffineWidget3D as AffineWidget3D
from .axes import Axes as Axes
from .axes_actor import AxesActor as AxesActor
from .axes_assembly import AxesAssembly as AxesAssembly
from .axes_assembly import AxesAssemblySymmetric as AxesAssemblySymmetric
from .axes_assembly import PlanesAssembly as PlanesAssembly
from .camera import Camera as Camera
from .charts import Chart2D as Chart2D
from .charts import ChartBox as ChartBox
from .charts import ChartMPL as ChartMPL
from .charts import ChartPie as ChartPie
from .colors import PARAVIEW_BACKGROUND as PARAVIEW_BACKGROUND
from .colors import Color as Color
from .colors import color_char_to_word as color_char_to_word
from .colors import get_cmap_safe as get_cmap_safe
from .colors import hexcolors as hexcolors
from .composite_mapper import BlockAttributes as BlockAttributes
from .composite_mapper import CompositeAttributes as CompositeAttributes
from .composite_mapper import CompositePolyDataMapper as CompositePolyDataMapper
from .cube_axes_actor import CubeAxesActor as CubeAxesActor
from .errors import InvalidCameraError as InvalidCameraError
from .errors import RenderWindowUnavailable as RenderWindowUnavailable
from .follower import Follower as Follower
from .helpers import plot_arrows as plot_arrows
from .helpers import plot_compare_four as plot_compare_four
from .lights import Light as Light
from .lookup_table import LookupTable as LookupTable
from .mapper import DataSetMapper as DataSetMapper
from .mapper import FixedPointVolumeRayCastMapper as FixedPointVolumeRayCastMapper
from .mapper import GPUVolumeRayCastMapper as GPUVolumeRayCastMapper
from .mapper import OpenGLGPUVolumeRayCastMapper as OpenGLGPUVolumeRayCastMapper
from .mapper import PointGaussianMapper as PointGaussianMapper
from .mapper import SmartVolumeMapper as SmartVolumeMapper
from .mapper import UnstructuredGridVolumeRayCastMapper as UnstructuredGridVolumeRayCastMapper
from .picking import PickingHelper as PickingHelper
from .plotter import _ALL_PLOTTERS as _ALL_PLOTTERS
from .plotter import BasePlotter as BasePlotter
from .plotter import Plotter as Plotter
from .plotter import close_all as close_all
from .prop3d import Prop3D as Prop3D
from .render_window_interactor import RenderWindowInteractor as RenderWindowInteractor
from .render_window_interactor import Timer as Timer
from .renderer import CameraPosition as CameraPosition
from .renderer import Renderer as Renderer
from .renderer import scale_point as scale_point
from .text import CornerAnnotation as CornerAnnotation
from .text import Label as Label
from .text import Text as Text
from .text import TextProperty as TextProperty
from .texture import Texture as Texture
from .texture import image_to_texture as image_to_texture
from .texture import numpy_to_texture as numpy_to_texture
from .themes import DocumentTheme as _GlobalTheme
from .themes import _set_plot_theme_from_env
from .themes import load_theme as load_theme
from .themes import set_plot_theme as set_plot_theme
from .tools import FONTS as FONTS
from .tools import check_math_text_support as check_math_text_support
from .tools import check_matplotlib_vtk_compatibility as check_matplotlib_vtk_compatibility
from .tools import create_axes_marker as create_axes_marker
from .tools import create_axes_orientation_box as create_axes_orientation_box
from .tools import normalize as normalize
from .tools import opacity_transfer_function as opacity_transfer_function
from .tools import parse_font_family as parse_font_family
from .tools import system_supports_plotting as system_supports_plotting
from .utilities import *
from .utilities.sphinx_gallery import _get_sg_image_scraper as _get_sg_image_scraper
from .volume import Volume as Volume
from .volume_property import VolumeProperty as VolumeProperty
from .widgets import WidgetHelper as WidgetHelper
class QtDeprecationError(Exception): # numpydoc ignore=PR01
"""Deprecation Error for features that moved to `pyvistaqt`."""
message = """`{}` has moved to pyvistaqt.
You can install this from PyPI with: `pip install pyvistaqt`
Then import it via: `from pyvistaqt import {}`
`{}` is no longer accessible by `pyvista.{}`
See https://github.com/pyvista/pyvistaqt
"""
def __init__(self, feature_name: str) -> None:
"""Empty init."""
Exception.__init__(self, self.message.format(*[feature_name] * 4))
class BackgroundPlotter: # numpydoc ignore=PR01
"""This class has been moved to pyvistaqt.""" # noqa: D404
def __init__(self, *args, **kwargs) -> None: # noqa: ARG002
"""Empty init."""
msg = 'BackgroundPlotter'
raise QtDeprecationError(msg)
class QtInteractor: # numpydoc ignore=PR01
"""This class has been moved to pyvistaqt.""" # noqa: D404
def __init__(self, *args, **kwargs) -> None: # noqa: ARG002
"""Empty init."""
msg = 'QtInteractor'
raise QtDeprecationError(msg)
global_theme: _GlobalTheme = _GlobalTheme()
# Set preferred plot theme
_set_plot_theme_from_env()
@@ -0,0 +1,296 @@
"""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,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
"""Type aliases for type hints."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import Literal
from typing import TypedDict
from typing import Union
import matplotlib as mpl
from pyvista.core._typing_core import BoundsTuple as BoundsTuple
from pyvista.core._typing_core import MatrixLike
from pyvista.core._typing_core import Number as Number
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import VectorLike
from . import _vtk
from .renderer import CameraPosition
if TYPE_CHECKING:
from pyvista.plotting.themes import Theme
from .charts import Chart2D as Chart2D
from .charts import ChartBox as ChartBox
from .charts import ChartMPL as ChartMPL
from .charts import ChartPie as ChartPie
from .colors import _CMCRAMERI_CMAPS_LITERAL
from .colors import _CMOCEAN_CMAPS_LITERAL
from .colors import _COLORCET_CMAPS_LITERAL
from .colors import _MATPLOTLIB_CMAPS_LITERAL
from .colors import Color as Color
NamedColormaps = Union[
'_MATPLOTLIB_CMAPS_LITERAL',
'_CMOCEAN_CMAPS_LITERAL',
'_COLORCET_CMAPS_LITERAL',
'_CMCRAMERI_CMAPS_LITERAL',
]
ColormapOptions = Union[NamedColormaps, list[str], mpl.colors.Colormap]
ColorLike = Union[
tuple[int, int, int],
tuple[int, int, int, int],
tuple[float, float, float],
tuple[float, float, float, float],
Sequence[int],
Sequence[float],
NumpyArray[float],
dict[str, Union[int, float, str]],
str,
'Color',
_vtk.vtkColor3ub,
]
# Overwrite default docstring, as sphinx is not able to capture the docstring
# when it is put beneath the definition somehow?
ColorLike.__doc__ = 'Any object convertible to a :class:`Color`.'
Chart = Union['Chart2D', 'ChartBox', 'ChartPie', 'ChartMPL']
FontFamilyOptions = Literal['courier', 'times', 'arial']
OpacityOptions = Literal[
'linear',
'linear_r',
'geom',
'geom_r',
'sigmoid',
'sigmoid_1',
'sigmoid_2',
'sigmoid_3',
'sigmoid_4',
'sigmoid_5',
'sigmoid_6',
'sigmoid_7',
'sigmoid_8',
'sigmoid_9',
'sigmoid_10',
'sigmoid_15',
'sigmoid_20',
'foreground',
]
CullingOptions = Literal['front', 'back', 'frontface', 'backface', 'f', 'b']
StyleOptions = Literal['surface', 'wireframe', 'points', 'points_gaussian']
LightingOptions = Literal['light kit', 'three lights', 'none']
CameraPositionOptions = Union[
Literal['xy', 'xz', 'yz', 'yx', 'zx', 'zy', 'iso'],
VectorLike[float],
MatrixLike[float],
CameraPosition,
]
class BackfaceArgs(TypedDict, total=False):
theme: Theme
interpolation: Literal['Physically based rendering', 'pbr', 'Phong', 'Gouraud', 'Flat']
color: ColorLike
style: StyleOptions
metallic: float
roughness: float
point_size: float
opacity: float
ambient: float
diffuse: float
specular: float
specular_power: float
show_edges: bool
edge_color: ColorLike
render_points_as_spheres: bool
render_lines_as_tubes: bool
lighting: bool
line_width: float
culling: CullingOptions | bool
edge_opacity: float
class ScalarBarArgs(TypedDict, total=False):
title: str
mapper: _vtk.vtkMapper
n_labels: int
italic: bool
bold: bool
title_font_size: float
label_font_size: float
color: ColorLike
font_family: FontFamilyOptions
shadow: bool
width: float
height: float
position_x: float
position_y: float
vertical: bool
interactive: bool
fmt: str
use_opacity: bool
outline: bool
nan_annotation: bool
below_label: str
above_label: str
background_color: ColorLike
n_colors: int
fill: bool
render: bool
theme: Theme
unconstrained_font_size: bool
class SilhouetteArgs(TypedDict, total=False):
color: ColorLike
line_width: float
opacity: float
feature_angle: float
decimate: float
@@ -0,0 +1,168 @@
"""All imports from VTK (including GL-dependent).
These are the modules within VTK that must be loaded across pyvista's
plotting API. Here, we attempt to import modules using the ``vtkmodules``
package, which lets us only have to import from select modules and not
the entire library.
"""
from __future__ import annotations
from vtkmodules.vtkChartsCore import vtkAxis as vtkAxis
from vtkmodules.vtkChartsCore import vtkChart as vtkChart
from vtkmodules.vtkChartsCore import vtkChartBox as vtkChartBox
from vtkmodules.vtkChartsCore import vtkChartPie as vtkChartPie
from vtkmodules.vtkChartsCore import vtkChartXY as vtkChartXY
from vtkmodules.vtkChartsCore import vtkChartXYZ as vtkChartXYZ
from vtkmodules.vtkChartsCore import vtkPlotArea as vtkPlotArea
from vtkmodules.vtkChartsCore import vtkPlotBar as vtkPlotBar
from vtkmodules.vtkChartsCore import vtkPlotBox as vtkPlotBox
from vtkmodules.vtkChartsCore import vtkPlotLine as vtkPlotLine
from vtkmodules.vtkChartsCore import vtkPlotLine3D as vtkPlotLine3D
from vtkmodules.vtkChartsCore import vtkPlotPie as vtkPlotPie
from vtkmodules.vtkChartsCore import vtkPlotPoints as vtkPlotPoints
from vtkmodules.vtkChartsCore import vtkPlotPoints3D as vtkPlotPoints3D
from vtkmodules.vtkChartsCore import vtkPlotStacked as vtkPlotStacked
from vtkmodules.vtkChartsCore import vtkPlotSurface as vtkPlotSurface
from vtkmodules.vtkCommonColor import vtkColorSeries as vtkColorSeries
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleImage as vtkInteractorStyleImage
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleJoystickActor as vtkInteractorStyleJoystickActor,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleJoystickCamera as vtkInteractorStyleJoystickCamera,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBand2D as vtkInteractorStyleRubberBand2D,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBandPick as vtkInteractorStyleRubberBandPick,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBandZoom as vtkInteractorStyleRubberBandZoom,
)
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTerrain as vtkInteractorStyleTerrain
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleTrackballActor as vtkInteractorStyleTrackballActor,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleTrackballCamera as vtkInteractorStyleTrackballCamera,
)
from vtkmodules.vtkInteractionWidgets import vtkBoxWidget as vtkBoxWidget
from vtkmodules.vtkInteractionWidgets import vtkButtonWidget as vtkButtonWidget
from vtkmodules.vtkInteractionWidgets import (
vtkDistanceRepresentation3D as vtkDistanceRepresentation3D,
)
from vtkmodules.vtkInteractionWidgets import vtkDistanceWidget as vtkDistanceWidget
from vtkmodules.vtkInteractionWidgets import vtkImplicitPlaneWidget as vtkImplicitPlaneWidget
from vtkmodules.vtkInteractionWidgets import vtkLineWidget as vtkLineWidget
from vtkmodules.vtkInteractionWidgets import vtkLogoRepresentation as vtkLogoRepresentation
from vtkmodules.vtkInteractionWidgets import vtkLogoWidget as vtkLogoWidget
from vtkmodules.vtkInteractionWidgets import (
vtkOrientationMarkerWidget as vtkOrientationMarkerWidget,
)
from vtkmodules.vtkInteractionWidgets import vtkPlaneWidget as vtkPlaneWidget
from vtkmodules.vtkInteractionWidgets import (
vtkPointHandleRepresentation3D as vtkPointHandleRepresentation3D,
)
from vtkmodules.vtkInteractionWidgets import vtkResliceCursorPicker as vtkResliceCursorPicker
from vtkmodules.vtkInteractionWidgets import vtkScalarBarWidget as vtkScalarBarWidget
from vtkmodules.vtkInteractionWidgets import vtkSliderRepresentation2D as vtkSliderRepresentation2D
from vtkmodules.vtkInteractionWidgets import vtkSliderWidget as vtkSliderWidget
from vtkmodules.vtkInteractionWidgets import vtkSphereWidget as vtkSphereWidget
from vtkmodules.vtkInteractionWidgets import vtkSplineWidget as vtkSplineWidget
from vtkmodules.vtkInteractionWidgets import (
vtkTexturedButtonRepresentation2D as vtkTexturedButtonRepresentation2D,
)
from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor as vtkAnnotatedCubeActor
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor as vtkAxesActor
from vtkmodules.vtkRenderingAnnotation import vtkAxisActor as vtkAxisActor
from vtkmodules.vtkRenderingAnnotation import vtkAxisActor2D as vtkAxisActor2D
from vtkmodules.vtkRenderingAnnotation import vtkCornerAnnotation as vtkCornerAnnotation
from vtkmodules.vtkRenderingAnnotation import vtkCubeAxesActor as vtkCubeAxesActor
from vtkmodules.vtkRenderingAnnotation import vtkLegendBoxActor as vtkLegendBoxActor
from vtkmodules.vtkRenderingAnnotation import vtkLegendScaleActor as vtkLegendScaleActor
from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor as vtkScalarBarActor
from vtkmodules.vtkRenderingContext2D import vtkBlockItem as vtkBlockItem
from vtkmodules.vtkRenderingContext2D import vtkBrush as vtkBrush
from vtkmodules.vtkRenderingContext2D import vtkContext2D as vtkContext2D
from vtkmodules.vtkRenderingContext2D import vtkContextActor as vtkContextActor
from vtkmodules.vtkRenderingContext2D import vtkContextScene as vtkContextScene
from vtkmodules.vtkRenderingContext2D import vtkImageItem as vtkImageItem
from vtkmodules.vtkRenderingContext2D import vtkPen as vtkPen
try:
from vtkmodules.vtkRenderingCore import vtkHardwarePicker as vtkHardwarePicker
except ImportError: # pragma: no cover
# VTK < 9.2 is missing this class
vtkHardwarePicker = None # type: ignore[assignment, misc] # noqa: N816
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_OFF as VTK_RESOLVE_OFF
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_POLYGON_OFFSET as VTK_RESOLVE_POLYGON_OFFSET
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_SHIFT_ZBUFFER as VTK_RESOLVE_SHIFT_ZBUFFER
from vtkmodules.vtkRenderingCore import vtkAbstractMapper as vtkAbstractMapper
from vtkmodules.vtkRenderingCore import vtkActor as vtkActor
from vtkmodules.vtkRenderingCore import vtkActor2D as vtkActor2D
from vtkmodules.vtkRenderingCore import vtkAreaPicker as vtkAreaPicker
from vtkmodules.vtkRenderingCore import vtkCamera as vtkCamera
from vtkmodules.vtkRenderingCore import vtkCellPicker as vtkCellPicker
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction as vtkColorTransferFunction
from vtkmodules.vtkRenderingCore import (
vtkCompositeDataDisplayAttributes as vtkCompositeDataDisplayAttributes,
)
from vtkmodules.vtkRenderingCore import vtkCompositePolyDataMapper as vtkCompositePolyDataMapper
from vtkmodules.vtkRenderingCore import vtkCoordinate as vtkCoordinate
from vtkmodules.vtkRenderingCore import vtkDataSetMapper as vtkDataSetMapper
from vtkmodules.vtkRenderingCore import vtkFollower as vtkFollower
from vtkmodules.vtkRenderingCore import vtkImageActor as vtkImageActor
from vtkmodules.vtkRenderingCore import vtkInteractorStyle as vtkInteractorStyle
from vtkmodules.vtkRenderingCore import vtkLight as vtkLight
from vtkmodules.vtkRenderingCore import vtkLightActor as vtkLightActor
from vtkmodules.vtkRenderingCore import vtkLightKit as vtkLightKit
from vtkmodules.vtkRenderingCore import vtkMapper as vtkMapper
from vtkmodules.vtkRenderingCore import vtkPointGaussianMapper as vtkPointGaussianMapper
from vtkmodules.vtkRenderingCore import vtkPointPicker as vtkPointPicker
from vtkmodules.vtkRenderingCore import vtkPolyDataMapper as vtkPolyDataMapper
from vtkmodules.vtkRenderingCore import vtkPolyDataMapper2D as vtkPolyDataMapper2D
from vtkmodules.vtkRenderingCore import vtkProp as vtkProp
from vtkmodules.vtkRenderingCore import vtkProp3D as vtkProp3D
from vtkmodules.vtkRenderingCore import vtkPropAssembly as vtkPropAssembly
from vtkmodules.vtkRenderingCore import vtkPropCollection as vtkPropCollection
from vtkmodules.vtkRenderingCore import vtkProperty as vtkProperty
from vtkmodules.vtkRenderingCore import vtkPropPicker as vtkPropPicker
from vtkmodules.vtkRenderingCore import vtkRenderedAreaPicker as vtkRenderedAreaPicker
from vtkmodules.vtkRenderingCore import vtkRenderer as vtkRenderer
from vtkmodules.vtkRenderingCore import vtkRenderWindow as vtkRenderWindow
from vtkmodules.vtkRenderingCore import vtkRenderWindowInteractor as vtkRenderWindowInteractor
from vtkmodules.vtkRenderingCore import vtkScenePicker as vtkScenePicker
from vtkmodules.vtkRenderingCore import vtkSelectVisiblePoints as vtkSelectVisiblePoints
from vtkmodules.vtkRenderingCore import vtkSkybox as vtkSkybox
from vtkmodules.vtkRenderingCore import vtkTextActor as vtkTextActor
from vtkmodules.vtkRenderingCore import vtkTextProperty as vtkTextProperty
from vtkmodules.vtkRenderingCore import vtkTexture as vtkTexture
from vtkmodules.vtkRenderingCore import vtkVolume as vtkVolume
from vtkmodules.vtkRenderingCore import vtkVolumeProperty as vtkVolumeProperty
from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter as vtkWindowToImageFilter
from vtkmodules.vtkRenderingCore import vtkWorldPointPicker as vtkWorldPointPicker
from vtkmodules.vtkRenderingFreeType import (
vtkMathTextFreeTypeTextRenderer as vtkMathTextFreeTypeTextRenderer,
)
from vtkmodules.vtkRenderingFreeType import vtkVectorText as vtkVectorText
from vtkmodules.vtkRenderingLabel import vtkLabelPlacementMapper as vtkLabelPlacementMapper
from vtkmodules.vtkRenderingLabel import vtkPointSetToLabelHierarchy as vtkPointSetToLabelHierarchy
from vtkmodules.vtkRenderingUI import (
vtkGenericRenderWindowInteractor as vtkGenericRenderWindowInteractor,
)
from vtkmodules.vtkRenderingVolume import (
vtkFixedPointVolumeRayCastMapper as vtkFixedPointVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolume import vtkGPUVolumeRayCastMapper as vtkGPUVolumeRayCastMapper
from vtkmodules.vtkRenderingVolume import (
vtkUnstructuredGridVolumeRayCastMapper as vtkUnstructuredGridVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolume import vtkVolumePicker as vtkVolumePicker
from vtkmodules.vtkViewsContext2D import vtkContextInteractorStyle as vtkContextInteractorStyle
from pyvista.core._vtk_core import *
from ._vtk_gl import *
@@ -0,0 +1,44 @@
"""GL-dependent imports from VTK.
These are the modules within VTK requiring libGL that must be loaded
across pyvista's plotting API. These imports have the potential to
raise an ImportError if the user does not have libGL installed.
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
"""
from __future__ import annotations
import contextlib
try:
# Necessary for displaying charts, otherwise crashes on rendering
from vtkmodules import vtkRenderingContextOpenGL2 as vtkRenderingContextOpenGL2
except ImportError: # pragma: no cover
vtkRenderingContextOpenGL2 = None # type: ignore[assignment] # noqa: N816
from vtkmodules.vtkRenderingOpenGL2 import vtkCameraPass as vtkCameraPass
with contextlib.suppress(ImportError):
from vtkmodules.vtkRenderingOpenGL2 import ( # type: ignore[attr-defined]
vtkCompositePolyDataMapper2 as vtkCompositePolyDataMapper2,
)
from vtkmodules.vtkRenderingOpenGL2 import vtkDepthOfFieldPass as vtkDepthOfFieldPass
from vtkmodules.vtkRenderingOpenGL2 import vtkEDLShading as vtkEDLShading
from vtkmodules.vtkRenderingOpenGL2 import vtkGaussianBlurPass as vtkGaussianBlurPass
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLFXAAPass as vtkOpenGLFXAAPass
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLHardwareSelector as vtkOpenGLHardwareSelector
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLRenderer as vtkOpenGLRenderer
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLTexture as vtkOpenGLTexture
from vtkmodules.vtkRenderingOpenGL2 import vtkRenderPassCollection as vtkRenderPassCollection
from vtkmodules.vtkRenderingOpenGL2 import vtkRenderStepsPass as vtkRenderStepsPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSequencePass as vtkSequencePass
from vtkmodules.vtkRenderingOpenGL2 import vtkShadowMapPass as vtkShadowMapPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSSAAPass as vtkSSAAPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSSAOPass as vtkSSAOPass
from vtkmodules.vtkRenderingVolumeOpenGL2 import (
vtkOpenGLGPUVolumeRayCastMapper as vtkOpenGLGPUVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper as vtkSmartVolumeMapper
@@ -0,0 +1,455 @@
"""Wrap :vtk:`vtkActor` module."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk
from ._property import Property
from .prop3d import Prop3D
if TYPE_CHECKING:
from typing_extensions import Self
from .mapper import _BaseMapper
class Actor(Prop3D, _vtk.vtkActor):
"""Wrap :vtk:`vtkActor`.
This class represents the geometry & properties in a rendered
scene. Normally, a :class:`pyvista.Actor` is constructed from
:func:`pyvista.Plotter.add_mesh`, but there may be times when it is more
convenient to construct an actor directly from a
:class:`pyvista.DataSetMapper`.
Parameters
----------
mapper : pyvista.DataSetMapper, optional
DataSetMapper.
prop : pyvista.Property, optional
Property of the actor.
name : str, optional
The name of this actor used when tracking on a plotter.
Examples
--------
Create an actor without using :class:`pyvista.Plotter`.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor
Actor (...)
Center: (0.0, 0.0, 0.0)
Pickable: True
Position: (0.0, 0.0, 0.0)
Scale: (1.0, 1.0, 1.0)
Visible: True
X Bounds -4.993E-01, 4.993E-01
Y Bounds -4.965E-01, 4.965E-01
Z Bounds -5.000E-01, 5.000E-01
User matrix: Identity
Has mapper: True
...
Change the actor properties and plot the actor.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor.prop.color = 'blue'
>>> actor.plot()
Create an actor using the :class:`pyvista.Plotter` and then change the
visibility of the actor.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> mesh = pv.Sphere()
>>> actor = pl.add_mesh(mesh)
>>> actor.visibility = False
>>> actor.visibility
False
"""
def __init__(self, mapper=None, prop=None, name=None) -> None:
"""Initialize actor."""
super().__init__()
if mapper is not None:
self.mapper = mapper
if prop is None:
self.prop = Property()
else:
self.prop = prop
self._name = name
@property
def mapper(self) -> _BaseMapper: # numpydoc ignore=RT01
"""Return or set the mapper of the actor.
Examples
--------
Create an actor and assign a mapper to it.
>>> import pyvista as pv
>>> dataset = pv.Sphere()
>>> actor = pv.Actor()
>>> actor.mapper = pv.DataSetMapper(dataset)
>>> actor.mapper
DataSetMapper (...)
Scalar visibility: True
Scalar range: (0.0, 1.0)
Interpolate before mapping: True
Scalar map mode: default
Color mode: direct
<BLANKLINE>
Attached dataset:
PolyData (...)
N Cells: 1680
N Points: 842
N Strips: 0
X Bounds: -4.993e-01, 4.993e-01
Y Bounds: -4.965e-01, 4.965e-01
Z Bounds: -5.000e-01, 5.000e-01
N Arrays: 1
"""
return self.GetMapper() # type: ignore[return-value]
@mapper.setter
def mapper(self, obj) -> None:
self.SetMapper(obj)
@property
def prop(self): # numpydoc ignore=RT01
"""Return or set the property of this actor.
Examples
--------
Modify the properties of an actor after adding a dataset to the plotter.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> prop = actor.prop
>>> prop.diffuse = 0.6
>>> pl.show()
"""
return self.GetProperty()
@prop.setter
def prop(self, obj: Property) -> None:
self.SetProperty(obj)
@property
def texture(self): # numpydoc ignore=RT01
"""Return or set the actor texture.
Notes
-----
The mapper dataset must have texture coordinates for the texture to be
used.
Examples
--------
Create an actor and add a texture to it. Note how the
:class:`pyvista.PolyData` has texture coordinates by default.
>>> import pyvista as pv
>>> from pyvista import examples
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates is not None
True
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane)
>>> actor.texture = examples.download_masonry_texture()
>>> actor.texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 256, 256
"""
return self.GetTexture()
@texture.setter
def texture(self, obj) -> None:
self.SetTexture(obj)
@property
def memory_address(self): # numpydoc ignore=RT01
"""Return the memory address of this actor."""
return self.GetAddressAsString('')
@property
def pickable(self) -> bool: # numpydoc ignore=RT01
"""Return or set actor pickability.
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then make the
actor unpickable.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> actor.pickable = False
>>> actor.pickable
False
"""
return bool(self.GetPickable())
@pickable.setter
def pickable(self, value) -> None:
self.SetPickable(value)
@property
def visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set actor visibility.
See Also
--------
use_bounds
pyvista.Plotter.compute_bounds
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then change the
visibility of the actor.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.load_airplane()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> pl.bounds
BoundsTuple(x_min = 139.06100463867188,
x_max = 1654.9300537109375,
y_min = 32.09429931640625,
y_max = 1319.949951171875,
z_min = -17.741199493408203,
z_max = 282.1300048828125)
>>> actor.visibility = False
>>> pl.bounds
BoundsTuple(x_min = -1.0,
x_max = 1.0,
y_min = -1.0,
y_max = 1.0,
z_min = -1.0,
z_max = 1.0)
"""
return bool(self.GetVisibility())
@visibility.setter
def visibility(self, value: bool) -> None:
self.SetVisibility(value)
@property
def use_bounds(self) -> bool: # numpydoc ignore=RT01
"""Return or set the use of actor's bounds.
.. versionadded:: 0.45
See Also
--------
visibility
pyvista.Plotter.compute_bounds
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then change the
use of bounds for the actor.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.load_airplane()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> pl.bounds
BoundsTuple(x_min = 139.06100463867188,
x_max = 1654.9300537109375,
y_min = 32.09429931640625,
y_max = 1319.949951171875,
z_min = -17.741199493408203,
z_max = 282.1300048828125)
>>> actor.use_bounds = False
>>> pl.bounds
BoundsTuple(x_min = -1.0,
x_max = 1.0,
y_min = -1.0,
y_max = 1.0,
z_min = -1.0,
z_max = 1.0)
Although the actor's bounds are no longer used, the actor remains visible.
>>> actor.visibility
True
"""
return bool(self.GetUseBounds())
@use_bounds.setter
def use_bounds(self, value: bool) -> None:
self.SetUseBounds(value)
def plot(self, **kwargs) -> None:
"""Plot just the actor.
This may be useful when interrogating or debugging individual actors.
Parameters
----------
**kwargs : dict, optional
Optional keyword arguments passed to :func:`pyvista.Plotter.show`.
Examples
--------
Create an actor without the :class:`pyvista.Plotter`, change its
properties, and plot it.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor.prop.color = 'red'
>>> actor.prop.show_edges = True
>>> actor.plot()
"""
pl = pyvista.Plotter()
pl.add_actor(self)
pl.show(**kwargs)
@_deprecate_positional_args
def copy(self: Self, deep: bool = True) -> Self: # noqa: FBT001, FBT002
"""Create a copy of this actor.
Parameters
----------
deep : bool, default: True
Create a shallow or deep copy of the actor. A deep copy will have a
new property and mapper, while a shallow copy will use the mapper
and property of this actor.
Returns
-------
Actor
Deep or shallow copy of this actor.
Examples
--------
Create an actor of a cube by adding it to a :class:`~pyvista.Plotter`
and then copy the actor, change the properties, and add it back to the
:class:`~pyvista.Plotter`.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh, color='b')
>>> new_actor = actor.copy()
>>> new_actor.prop.style = 'wireframe'
>>> new_actor.prop.line_width = 5
>>> new_actor.prop.color = 'r'
>>> new_actor.prop.lighting = False
>>> _ = pl.add_actor(new_actor)
>>> pl.show()
"""
new_actor = type(self)()
if deep:
if self.mapper is not None:
new_actor.mapper = self.mapper.copy()
new_actor.prop = self.prop.copy()
else:
new_actor.ShallowCopy(self)
return new_actor
def __repr__(self):
"""Representation of the actor."""
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
bnd = self.bounds
attr = [
f'{type(self).__name__} ({hex(id(self))})',
f' Center: {self.center}',
f' Pickable: {self.pickable}',
f' Position: {self.position}',
f' Scale: {self.scale}',
f' Visible: {self.visibility}',
f' X Bounds {bnd[0]:.3E}, {bnd[1]:.3E}',
f' Y Bounds {bnd[2]:.3E}, {bnd[3]:.3E}',
f' Z Bounds {bnd[4]:.3E}, {bnd[5]:.3E}',
f' User matrix: {mat_info}',
f' Has mapper: {self.mapper is not None}',
'',
repr(self.prop),
]
if self.mapper is not None:
attr.append('')
attr.append(repr(self.mapper))
return '\n'.join(attr)
@property
def backface_prop(self) -> pyvista.Property | None: # numpydoc ignore=RT01
"""Return or set the backface property.
By default this property matches the frontface property
:attr:`Actor.prop`. Once accessed or modified, this backface
property becomes independent of the frontface property. In
order to restore the fallback to frontface property, assign
``None`` to the property.
Returns
-------
pyvista.Property
The object describing backfaces.
See Also
--------
:ref:`backface_prop_example`
Examples
--------
Clip a sphere by a plane and color the inside of the clipped sphere
light blue using the ``backface_prop``.
>>> import numpy as np
>>> import pyvista as pv
>>> plane = pv.Plane(i_size=1.5, j_size=1.5)
>>> mesh = pv.Sphere().clip_surface(plane, invert=False)
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh, smooth_shading=True)
>>> actor.backface_prop.color = 'lightblue'
>>> _ = pl.add_mesh(
... plane,
... opacity=0.25,
... show_edges=True,
... color='grey',
... lighting=False,
... )
>>> pl.show()
"""
if self.GetBackfaceProperty() is None:
self.SetBackfaceProperty(self.prop.copy())
return self.GetBackfaceProperty() # type: ignore[return-value]
@backface_prop.setter
def backface_prop(self, value: pyvista.Property) -> None:
self.SetBackfaceProperty(value)
@@ -0,0 +1,152 @@
"""Module containing pyvista implementation of :vtk:`vtkProperty`."""
from __future__ import annotations
from typing import TYPE_CHECKING
from pyvista.core.utilities.misc import _NoNewAttrMixin
from .opts import InterpolationType
from .opts import RepresentationType
if TYPE_CHECKING:
from . import _vtk
class ActorProperties(_NoNewAttrMixin):
"""Properties wrapper for :vtk:`vtkProperty`.
Contains the surface properties of the object.
Parameters
----------
properties : :vtk:`vtkProperty`
VTK properties of the current object.
Examples
--------
Access the properties of the z-axis shaft.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> z_axes_prop = axes.axes_actor.z_axis_shaft_properties
>>> z_axes_prop.color = (1.0, 1.0, 0.0)
>>> z_axes_prop.opacity = 0.5
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.CYLINDER
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes.axes_actor)
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.show()
"""
def __init__(self, properties: _vtk.vtkProperty) -> None:
super().__init__()
self.properties = properties
@property
def color(self): # numpydoc ignore=RT01
"""Return or set the color of the actor."""
return self.properties.GetColor()
@color.setter
def color(self, color: tuple[float, float, float]):
self.properties.SetColor(color[0], color[1], color[2])
@property
def metallic(self): # numpydoc ignore=RT01
"""Return or set the metallic coefficient of the surface."""
return self.properties.GetMetallic()
@metallic.setter
def metallic(self, value: float):
self.properties.SetMetallic(value)
@property
def roughness(self): # numpydoc ignore=RT01
"""Return or set the roughness of the surface."""
return self.properties.GetRoughness()
@roughness.setter
def roughness(self, value: float):
self.properties.SetRoughness(value)
@property
def anisotropy(self): # numpydoc ignore=RT01
"""Return or set the anisotropy coefficient."""
return self.properties.GetAnisotropy()
@anisotropy.setter
def anisotropy(self, value: float):
self.properties.SetAnisotropy(value)
@property
def anisotropy_rotation(self): # numpydoc ignore=RT01
"""Return or set the anisotropy rotation coefficient."""
return self.properties.GetAnisotropyRotation()
@anisotropy_rotation.setter
def anisotropy_rotation(self, value: float):
self.properties.SetAnisotropyRotation(value)
@property
def lighting(self): # numpydoc ignore=RT01
"""Return or set the lighting activation flag."""
return self.properties.GetLighting()
@lighting.setter
def lighting(self, flag: bool):
self.properties.SetLighting(flag)
@property
def interpolation_model(self): # numpydoc ignore=RT01
"""Return or set the interpolation model.
Can be any of the options in :class:`pyvista.plotting.opts.InterpolationType` enum.
"""
return InterpolationType.from_any(self.properties.GetInterpolation())
@interpolation_model.setter
def interpolation_model(self, model: InterpolationType):
self.properties.SetInterpolation(model.value)
@property
def index_of_refraction(self): # numpydoc ignore=RT01
"""Return or set the Index Of Refraction of the base layer."""
return self.properties.GetBaseIOR()
@index_of_refraction.setter
def index_of_refraction(self, value: float):
self.properties.SetBaseIOR(value)
@property
def opacity(self): # numpydoc ignore=RT01
"""Return or set the opacity of the actor."""
return self.properties.GetOpacity()
@opacity.setter
def opacity(self, value: float):
self.properties.SetOpacity(value)
@property
def shading(self): # numpydoc ignore=RT01
"""Return or set the flag to activate the shading."""
return self.properties.GetShading()
@shading.setter
def shading(self, is_active: bool):
self.properties.SetShading(is_active)
@property
def representation(self) -> RepresentationType: # numpydoc ignore=RT01
"""Return or set the representation of the actor.
Can be any of the options in :class:`pyvista.plotting.opts.RepresentationType` enum.
"""
return RepresentationType.from_any(self.properties.GetRepresentation())
@representation.setter
def representation(self, value: RepresentationType):
self.properties.SetRepresentation(RepresentationType.from_any(value).value)
@@ -0,0 +1,538 @@
"""Affine widget module."""
from __future__ import annotations
from typing import cast
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.errors import VTKVersionError
from pyvista.core.utilities.misc import _NoNewAttrMixin
from pyvista.core.utilities.misc import try_callback
from . import _vtk
DARK_YELLOW = (0.9647058823529412, 0.7450980392156863, 0)
GLOBAL_AXES = np.eye(3)
def _validate_axes(axes):
"""Validate and normalize input axes.
Axes are expected to follow the right-hand rule (e.g. third axis is the
cross product of the first two.
Parameters
----------
axes : sequence
The axes to be validated and normalized. Should be of shape (3, 3).
Returns
-------
dict
The validated and normalized axes.
"""
axes = np.array(axes)
if axes.shape != (3, 3):
msg = '`axes` must be a (3, 3) array.'
raise ValueError(msg)
axes = axes / np.linalg.norm(axes, axis=1, keepdims=True)
if not np.allclose(np.cross(axes[0], axes[1]), axes[2]):
msg = '`axes` do not follow the right hand rule.'
raise ValueError(msg)
return axes
def _check_callable(func, name='callback'):
"""Check if a variable is callable."""
if func and not callable(func):
msg = f'`{name}` must be a callable, not {type(func)}.'
raise TypeError(msg)
return func
def _make_quarter_arc():
"""Make a quarter circle centered at the origin."""
circ = pyvista.Circle(resolution=100)
circ.faces = np.empty(0, dtype=int)
circ.lines = np.hstack(([26], np.arange(0, 26)))
return circ
def get_angle(v1, v2):
"""Compute the angle between two vectors in degrees.
Parameters
----------
v1 : numpy.ndarray
First input vector.
v2 : numpy.ndarray
Second input vector.
Returns
-------
float
Angle between vectors in degrees.
"""
return np.rad2deg(np.arccos(np.clip(np.dot(v1, v2), -1.0, 1.0)))
@_deprecate_positional_args
def ray_plane_intersection(start_point, direction, plane_point, normal): # noqa: PLR0917
"""Compute the intersection between a ray and a plane.
Parameters
----------
start_point : ndarray
Starting point of the ray.
direction : ndarray
Direction of the ray.
plane_point : ndarray
A point on the plane.
normal : ndarray
Normal to the plane.
Returns
-------
ndarray
Intersection point.
"""
t_value = np.dot(normal, (plane_point - start_point)) / np.dot(normal, direction)
return start_point + t_value * direction
class AffineWidget3D(_NoNewAttrMixin):
"""3D affine transform widget.
This widget allows interactive transformations including translation and
rotation using the left mouse button.
Parameters
----------
plotter : pyvista.Plotter
The plotter object.
actor : pyvista.Actor
The actor to which the widget is attached to.
origin : sequence[float], optional
Origin of the widget. Default is the center of the main actor.
start : bool, default: True
If True, start the widget immediately.
scale : float, default: 0.15
Scale factor for the widget relative to the length of the actor.
line_radius : float, default: 0.02
Relative radius of the lines composing the widget.
always_visible : bool, default: True
Make the widget always visible. Setting this to ``False`` will cause
the widget geometry to be hidden by other actors in the plotter.
axes_colors : tuple[ColorLike], optional
Uses the theme by default. Configure the individual axis colors by
modifying either the theme with ``pyvista.global_theme.axes.x_color =
<COLOR>`` or setting this with a ``tuple`` as in ``('r', 'g', 'b')``.
axes : numpy.ndarray, optional
``(3, 3)`` Numpy array defining the X, Y, and Z axes. By default this
matches the default coordinate system.
release_callback : callable, optional
Call this method when releasing the left mouse button. It is passed the
``user_matrix`` of the actor.
interact_callback : callable, optional
Call this method when moving the mouse with the left mouse button
pressed down and a valid movement actor selected. It is passed the
``user_matrix`` of the actor.
Notes
-----
After interacting with the actor, the transform will be stored within
:attr:`pyvista.Prop3D.user_matrix` but will not be applied to the
dataset. Use this matrix in conjunction with
:func:`pyvista.DataObjectFilters.transform` to transform the dataset.
Requires VTK >= v9.2
Examples
--------
Create the affine widget outside of the plotter and add it.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> widget = pv.AffineWidget3D(pl, actor)
>>> pl.show()
Access the transform from the actor.
>>> actor.user_matrix
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
"""
@_deprecate_positional_args(allowed=['plotter', 'actor'])
def __init__( # noqa: PLR0917
self,
plotter,
actor,
origin=None,
start: bool = True, # noqa: FBT001, FBT002
scale=0.15,
line_radius=0.02,
always_visible: bool = True, # noqa: FBT001, FBT002
axes_colors=None,
axes=None,
release_callback=None,
interact_callback=None,
):
"""Initialize the widget."""
# needs VTK v9.2.0 due to the hardware picker
if pyvista.vtk_version_info < (9, 2):
msg = 'AfflineWidget3D requires VTK v9.2.0 or newer.'
raise VTKVersionError(msg)
self._axes = np.eye(4)
self._axes_inv = np.eye(4)
self._pl = plotter
self._main_actor = actor
self._selected_actor: pyvista.Actor | None = None
self._init_position = None
self._mouse_move_observer: int | None = None
self._left_press_observer: int | None = None
self._left_release_observer: int | None = None
if self._main_actor.user_matrix is None:
self._main_actor.user_matrix = np.eye(4)
self._cached_matrix = self._main_actor.user_matrix
self._arrows = [] # type: ignore[var-annotated]
self._circles = [] # type: ignore[var-annotated]
self._pressing_down = False
origin = origin or actor.center
self._origin = np.array(origin)
if axes_colors is None:
axes_colors = (
pyvista.global_theme.axes.x_color,
pyvista.global_theme.axes.y_color,
pyvista.global_theme.axes.z_color,
)
self._axes_colors = axes_colors
self._circ = _make_quarter_arc()
self._actor_length = self._main_actor.GetLength()
self._line_radius = line_radius
self._user_interact_callback = _check_callable(interact_callback)
self._user_release_callback = _check_callable(release_callback)
self._init_actors(scale, always_visible)
# axes must be set after initializing actors
if axes is not None:
try:
_validate_axes(axes)
except ValueError:
for actor_ in self._arrows + self._circles:
self._pl.remove_actor(actor_)
raise
self.axes = axes
if start:
self.enable()
def _init_actors(self, scale, always_visible):
"""Initialize the widget's actors."""
for ii, color in enumerate(self._axes_colors):
arrow = pyvista.Arrow(
start=(0, 0, 0),
direction=GLOBAL_AXES[ii],
scale=self._actor_length * scale * 1.15,
tip_radius=0.05,
shaft_radius=self._line_radius,
)
self._arrows.append(
self._pl.add_mesh(arrow, color=color, lighting=False, render=False)
)
axis_circ = self._circ.copy()
if ii == 0:
axis_circ = axis_circ.rotate_y(-90)
elif ii == 1:
axis_circ = axis_circ.rotate_x(90)
axis_circ.points *= self._main_actor.GetLength() * (scale * 1.6)
# axis_circ.points += self._origin
axis_circ = axis_circ.tube(
radius=self._line_radius * self._actor_length * scale,
absolute=True,
radius_factor=1.0,
)
self._circles.append(
self._pl.add_mesh(
axis_circ,
color=color,
lighting=False,
render_lines_as_tubes=True,
render=False,
),
)
# update origin and assign a default user_matrix
for actor in self._arrows + self._circles:
matrix = np.eye(4)
matrix[:3, -1] = self._origin
actor.user_matrix = matrix
if always_visible:
for actor in self._arrows + self._circles:
actor.mapper.SetResolveCoincidentTopologyToPolygonOffset()
actor.mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters(0, -20000)
def _get_world_coord_rot(self, interactor):
"""Get the world coordinates given an interactor.
Unlike ``_get_world_coord_trans``, these coordinates are physically
accurate, but sensitive to the position of the camera. Rotation is zoom
independent.
"""
x, y = interactor.GetEventPosition()
coordinate = _vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(x, y, 0)
ren = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer()
point = np.array(coordinate.GetComputedWorldValue(ren))
if self._selected_actor:
index = self._circles.index(self._selected_actor)
to_widget = np.array(ren.camera.position - self._origin)
point = ray_plane_intersection(
start_point=point,
direction=to_widget,
plane_point=self._origin,
normal=self.axes[index],
)
return point
def _get_world_coord_trans(self, interactor):
"""Get the world coordinates given an interactor.
This uses a modified scaled approach to get the world coordinates that
are not physically accurate, but ignores zoom and works for
translation.
"""
x, y = interactor.GetEventPosition()
ren = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer()
# Get normalized view coordinates (-1, 1)
width, height = ren.GetSize()
ndc_x = 2 * (x / width) - 1
ndc_y = 2 * (y / height) - 1
ndc_z = 1
# convert camera coordinates to world coordinates
camera = ren.GetActiveCamera()
projection_matrix = pyvista.array_from_vtkmatrix(
camera.GetProjectionTransformMatrix(ren.GetTiledAspectRatio(), 0, 1),
)
inverse_projection_matrix = np.linalg.inv(projection_matrix)
camera_coords = np.dot(inverse_projection_matrix, [ndc_x, ndc_y, ndc_z, 1])
modelview_matrix = pyvista.array_from_vtkmatrix(camera.GetModelViewTransformMatrix())
inverse_modelview_matrix = np.linalg.inv(modelview_matrix)
world_coords = np.dot(inverse_modelview_matrix, camera_coords)
# Scale by twice actor length (experimentally determined for good UX)
return world_coords[:3] * self._actor_length * 2
def _move_callback(self, interactor, _event):
"""Process actions for the move mouse event."""
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)
actor = picker.GetActor()
if self._pressing_down:
if self._selected_actor in self._arrows:
current_pos = self._get_world_coord_trans(interactor)
index = self._arrows.index(self._selected_actor)
diff = current_pos - self._init_position
trans_matrix = np.eye(4)
trans_matrix[:3, -1] = self.axes[index] * np.dot(diff, self.axes[index])
matrix = trans_matrix @ self._cached_matrix
elif self._selected_actor in self._circles:
current_pos = self._get_world_coord_rot(interactor)
index = self._circles.index(self._selected_actor)
vec_current = current_pos - self._origin
vec_init = self._init_position - self._origin
normal = self.axes[index]
vec_current = vec_current - np.dot(vec_current, normal) * normal
vec_init = vec_init - np.dot(vec_init, normal) * normal
vec_current /= np.linalg.norm(vec_current)
vec_init /= np.linalg.norm(vec_init)
angle = get_angle(vec_init, vec_current)
cross = np.cross(vec_init, vec_current)
if cross[index] < 0:
angle = -angle
trans = _vtk.vtkTransform()
trans.Translate(self._origin) # type: ignore[call-overload]
trans.RotateWXYZ(
angle,
self._axes[index][0],
self._axes[index][1],
self._axes[index][2],
)
trans.Translate(-self._origin) # type: ignore[call-overload]
trans.Update()
rot_matrix = pyvista.array_from_vtkmatrix(trans.GetMatrix())
matrix = rot_matrix @ self._cached_matrix
if self._user_interact_callback:
try_callback(self._user_interact_callback, self._main_actor.user_matrix)
self._main_actor.user_matrix = matrix
elif self._selected_actor and self._selected_actor is not actor:
# Return the color of the currently selected actor to normal and
# deselect it
if self._selected_actor in self._arrows:
index = self._arrows.index(self._selected_actor)
elif self._selected_actor in self._circles:
index = self._circles.index(self._selected_actor)
self._selected_actor.prop.color = self._axes_colors[index]
self._selected_actor = None
# Highlight the actor if there is no selected actor
if actor and not self._selected_actor:
if actor in self._arrows:
index = self._arrows.index(actor)
self._arrows[index].prop.color = DARK_YELLOW
actor.prop.color = DARK_YELLOW
self._selected_actor = actor
elif actor in self._circles:
index = self._circles.index(actor)
self._circles[index].prop.color = DARK_YELLOW
actor.prop.color = DARK_YELLOW
self._selected_actor = actor
self._pl.render()
def _press_callback(self, interactor, _event):
"""Process actions for the mouse button press event."""
if self._selected_actor:
self._pl.enable_trackball_actor_style()
self._pressing_down = True
if self._selected_actor in self._circles:
self._init_position = self._get_world_coord_rot(interactor)
else:
self._init_position = self._get_world_coord_trans(interactor)
def _release_callback(self, _interactor, _event):
"""Process actions for the mouse button release event."""
self._pl.enable_trackball_style()
self._pressing_down = False
self._cached_matrix = self._main_actor.user_matrix
if self._user_release_callback:
try_callback(self._user_release_callback, self._main_actor.user_matrix)
def _reset(self):
"""Reset the actor and cached transform."""
self._main_actor.user_matrix = np.eye(4)
self._cached_matrix = np.eye(4)
@property
def axes(self):
"""Return or set the axes of the widget.
The axes will be checked for orthogonality. Non-orthogonal axes will
raise a ``ValueError``
Returns
-------
numpy.ndarray
``(3, 3)`` array of axes.
"""
return self._axes[:3, :3]
@axes.setter
def axes(self, axes):
mat = np.eye(4)
mat[:3, :3] = _validate_axes(axes)
mat[:3, -1] = self.origin
self._axes = mat
self._axes_inv = np.linalg.inv(self._axes) # type: ignore[assignment]
for actor in self._arrows + self._circles:
matrix = actor.user_matrix
# Be sure to use the inverse here
matrix[:3, :3] = self._axes_inv[:3, :3]
actor.user_matrix = matrix
@property
def origin(self) -> tuple[float, float, float]:
"""Origin of the widget.
This is where the origin of the widget will be located and where the
actor will be rotated about.
Returns
-------
tuple
Widget origin.
"""
return cast('tuple[float, float, float]', tuple(self._origin))
@origin.setter
def origin(self, value):
value = np.array(value)
diff = value - self._origin
for actor in self._circles + self._arrows:
if actor.user_matrix is None:
actor.user_matrix = np.eye(4)
matrix = actor.user_matrix
matrix[:3, -1] += diff
actor.user_matrix = matrix
self._origin = value
def enable(self):
"""Enable the widget."""
if not self._pl._picker_in_use:
self._pl.enable_mesh_picking(show_message=False, show=False, picker='hardware')
self._mouse_move_observer = self._pl.iren.add_observer(
'MouseMoveEvent',
self._move_callback,
)
self._left_press_observer = self._pl.iren.add_observer(
'LeftButtonPressEvent',
self._press_callback,
interactor_style_fallback=False,
)
self._left_release_observer = self._pl.iren.add_observer(
'LeftButtonReleaseEvent',
self._release_callback,
interactor_style_fallback=False,
)
def disable(self):
"""Disable the widget."""
self._pl.disable_picking()
if self._mouse_move_observer:
self._pl.iren.remove_observer(self._mouse_move_observer)
if self._left_press_observer:
self._pl.iren.remove_observer(self._left_press_observer)
if self._left_release_observer:
self._pl.iren.remove_observer(self._left_release_observer)
def remove(self):
"""Disable and delete all actors of this widget."""
self.disable()
for actor in self._circles + self._arrows:
self._pl.remove_actor(actor)
self._circles = []
self._arrows = []
@@ -0,0 +1,136 @@
"""Module containing pyvista implementation of :vtk:`vtkAxes`."""
from __future__ import annotations
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .actor import Actor
from .axes_actor import AxesActor
class Axes(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkAxes):
"""PyVista wrapper for the VTK Axes class.
Parameters
----------
show_actor : bool, optional
Hide or show the actor of these axes. Default ``False``.
actor_scale : float, optional
Scale the size of the axes actor. Default ``1``.
line_width : float, optional
Width of the axes lines. Default ``1``.
symmetric : bool, optional
If true, the axis continue to negative values.
Examples
--------
Create an instance of axes at the pyvista module level.
>>> import pyvista as pv
>>> axes = pv.Axes()
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self,
show_actor: bool = False, # noqa: FBT001, FBT002
actor_scale=1,
line_width=1.0,
symmetric: bool = False, # noqa: FBT001, FBT002
): # numpydoc ignore=PR01,RT01
"""Initialize a new axes descriptor."""
super().__init__()
self.SetSymmetric(symmetric)
# Add the axes mapper
self.mapper = _vtk.vtkPolyDataMapper()
self.mapper.SetInputConnection(self.GetOutputPort())
# Add the axes actor
self.actor = Actor(mapper=self.mapper)
self.axes_actor = AxesActor()
self.actor.visibility = show_actor
self.actor.scale = actor_scale
self.actor.prop.line_width = line_width
@property
def origin(self): # numpydoc ignore=RT01
"""Return or set th origin of the axes in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.origin
(0.0, 0.0, 0.0)
Set the origin of the camera.
>>> axes.origin = (2.0, 1.0, 1.0)
>>> axes.origin
(2.0, 1.0, 1.0)
"""
return self.GetOrigin()
@origin.setter
def origin(self, value):
self.SetOrigin(value)
def show_actor(self):
"""Show an actor of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.show_actor()
"""
self.actor.visibility = True
def hide_actor(self):
"""Hide an actor of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.hide_actor()
"""
self.actor.visibility = False
def show_symmetric(self):
"""Show symmetric of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.show_symmetric()
"""
self.SymmetricOn()
def hide_symmetric(self):
"""Hide symmetric of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.hide_symmetric()
"""
self.SymmetricOff()
def __del__(self):
"""Clean the attributes of the class."""
self.axes_actor = None # type: ignore[assignment]
self.actor = None # type: ignore[assignment]
self.mapper = None # type: ignore[assignment]
@@ -0,0 +1,665 @@
"""Axes actor module."""
from __future__ import annotations
from collections.abc import Iterable
from enum import Enum
import warnings
import pyvista
from pyvista.core._typing_core import BoundsTuple
from pyvista.core.errors import PyVistaDeprecationWarning
from pyvista.core.utilities.misc import _BoundsSizeMixin
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .actor_properties import ActorProperties
class AxesActor(
_NoNewAttrMixin, _NameMixin, _BoundsSizeMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkAxesActor
):
"""Axes actor wrapper for :vtk:`vtkAxesActor`.
Hybrid 2D/3D actor used to represent 3D axes in a scene. The user
can define the geometry to use for the shaft or the tip, and the
user can set the text for the three axes. To see full customization
options, refer to :vtk:`vtkAxesActor`.
See Also
--------
:class:`~pyvista.AxesAssembly`
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Customize the axis shaft color and shape.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.z_axis_shaft_properties.color = (0.0, 1.0, 1.0)
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.CYLINDER
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes.axes_actor)
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.show()
Or you can use this as a custom orientation widget with
:func:`add_orientation_widget() <pyvista.Renderer.add_orientation_widget>`:
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes_actor = axes.axes_actor
>>> axes.axes_actor.shaft_type = 0
>>> axes_actor.x_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.y_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.z_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.x_label = 'U'
>>> axes_actor.y_label = 'V'
>>> axes_actor.z_label = 'W'
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Cone())
>>> _ = pl.add_orientation_widget(
... axes_actor,
... viewport=(0, 0, 0.5, 0.5),
... )
>>> pl.show()
"""
class ShaftType(Enum):
"""Types of shaft shapes available."""
CYLINDER = 0
LINE = 1
class TipType(Enum):
"""Types of tip shapes available."""
CONE = 0
SPHERE = 1
def __init__(self):
"""Initialize actor."""
super().__init__()
self.x_axis_shaft_properties.color = pyvista.global_theme.axes.x_color.float_rgb
self.x_axis_tip_properties.color = pyvista.global_theme.axes.x_color.float_rgb
self.x_axis_shaft_properties.opacity = pyvista.global_theme.axes.x_color.float_rgba[3]
self.x_axis_tip_properties.opacity = pyvista.global_theme.axes.x_color.float_rgba[3]
self.x_axis_shaft_properties.lighting = pyvista.global_theme.lighting
self.y_axis_shaft_properties.color = pyvista.global_theme.axes.y_color.float_rgb
self.y_axis_tip_properties.color = pyvista.global_theme.axes.y_color.float_rgb
self.y_axis_shaft_properties.opacity = pyvista.global_theme.axes.y_color.float_rgba[3]
self.y_axis_tip_properties.opacity = pyvista.global_theme.axes.y_color.float_rgba[3]
self.y_axis_shaft_properties.lighting = pyvista.global_theme.lighting
self.z_axis_shaft_properties.color = pyvista.global_theme.axes.z_color.float_rgb
self.z_axis_tip_properties.color = pyvista.global_theme.axes.z_color.float_rgb
self.z_axis_shaft_properties.opacity = pyvista.global_theme.axes.z_color.float_rgba[3]
self.z_axis_tip_properties.opacity = pyvista.global_theme.axes.z_color.float_rgba[3]
self.z_axis_shaft_properties.lighting = pyvista.global_theme.lighting
@property
def bounds(self) -> BoundsTuple:
"""Return the bounding box of this.
Returns
-------
BoundsTuple
Bounding box.
The form is: ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
"""
return BoundsTuple(*self.GetBounds())
@property
def center(self) -> tuple[float, float, float]:
"""Return the center.
Returns
-------
tuple[float, float, float]
Center of axes actor.
"""
return self.GetCenter()
@property
def visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set AxesActor visibility.
Examples
--------
Create an Axes object and then access the
visibility attribute of its AxesActor.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.visibility
True
"""
return bool(self.GetVisibility())
@visibility.setter
def visibility(self, value: bool):
self.SetVisibility(value)
@property
def total_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of all axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.total_length
(1.0, 1.0, 1.0)
>>> axes.axes_actor.total_length = 1.2
>>> axes.axes_actor.total_length
(1.2, 1.2, 1.2)
>>> axes.axes_actor.total_length = (1.0, 0.9, 0.5)
>>> axes.axes_actor.total_length
(1.0, 0.9, 0.5)
"""
return self.GetTotalLength()
@total_length.setter
def total_length(self, length):
if isinstance(length, Iterable):
self.SetTotalLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetTotalLength(length, length, length)
@property
def shaft_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of the axes shaft.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.shaft_length
(0.8, 0.8, 0.8)
>>> axes.axes_actor.shaft_length = 0.7
>>> axes.axes_actor.shaft_length
(0.7, 0.7, 0.7)
>>> axes.axes_actor.shaft_length = (1.0, 0.9, 0.5)
>>> axes.axes_actor.shaft_length
(1.0, 0.9, 0.5)
"""
return self.GetNormalizedShaftLength()
@shaft_length.setter
def shaft_length(self, length):
if isinstance(length, Iterable):
self.SetNormalizedShaftLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedShaftLength(length, length, length)
@property
def tip_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of the tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.tip_length
(0.2, 0.2, 0.2)
>>> axes.axes_actor.tip_length = 0.3
>>> axes.axes_actor.tip_length
(0.3, 0.3, 0.3)
>>> axes.axes_actor.tip_length = (0.1, 0.4, 0.2)
>>> axes.axes_actor.tip_length
(0.1, 0.4, 0.2)
"""
return self.GetNormalizedTipLength()
@tip_length.setter
def tip_length(self, length):
if isinstance(length, Iterable):
self.SetNormalizedTipLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedTipLength(length, length, length)
@property
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label along the axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.label_position
(1.0, 1.0, 1.0)
>>> axes.axes_actor.label_position = 0.3
>>> axes.axes_actor.label_position
(0.3, 0.3, 0.3)
>>> axes.axes_actor.label_position = (0.1, 0.4, 0.2)
>>> axes.axes_actor.label_position
(0.1, 0.4, 0.2)
"""
return self.GetNormalizedLabelPosition()
@label_position.setter
def label_position(self, length):
if isinstance(length, Iterable):
self.SetNormalizedLabelPosition(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedLabelPosition(length, length, length)
@property
def cone_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the cone tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cone_resolution
16
>>> axes.axes_actor.cone_resolution = 24
>>> axes.axes_actor.cone_resolution
24
"""
return self.GetConeResolution()
@cone_resolution.setter
def cone_resolution(self, res: int):
self.SetConeResolution(res)
@property
def sphere_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the spherical tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.sphere_resolution
16
>>> axes.axes_actor.sphere_resolution = 24
>>> axes.axes_actor.sphere_resolution
24
"""
return self.GetSphereResolution()
@sphere_resolution.setter
def sphere_resolution(self, res: int):
self.SetSphereResolution(res)
@property
def cylinder_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the shaft cylinder.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cylinder_resolution
16
>>> axes.axes_actor.cylinder_resolution = 24
>>> axes.axes_actor.cylinder_resolution
24
"""
return self.GetCylinderResolution()
@cylinder_resolution.setter
def cylinder_resolution(self, res: int):
self.SetCylinderResolution(res)
@property
def cone_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the cone tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cone_radius
0.4
>>> axes.axes_actor.cone_radius = 0.8
>>> axes.axes_actor.cone_radius
0.8
"""
return self.GetConeRadius()
@cone_radius.setter
def cone_radius(self, rad: float):
self.SetConeRadius(rad)
@property
def sphere_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the spherical tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.sphere_radius
0.4
>>> axes.axes_actor.sphere_radius = 0.8
>>> axes.axes_actor.sphere_radius
0.8
"""
return self.GetSphereRadius()
@sphere_radius.setter
def sphere_radius(self, rad: float):
self.SetSphereRadius(rad)
@property
def cylinder_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the shaft cylinder.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cylinder_radius
0.05
>>> axes.axes_actor.cylinder_radius = 0.03
>>> axes.axes_actor.cylinder_radius
0.03
"""
return self.GetCylinderRadius()
@cylinder_radius.setter
def cylinder_radius(self, rad: float):
self.SetCylinderRadius(rad)
@property
def shaft_type(self) -> ShaftType: # numpydoc ignore=RT01
"""Return or set the shaft type.
Can be either a cylinder(0) or a line(1).
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.LINE
>>> axes.axes_actor.shaft_type
<ShaftType.LINE: 1>
"""
return AxesActor.ShaftType(self.GetShaftType())
@shaft_type.setter
def shaft_type(self, shaft_type: ShaftType | int):
shaft_type = AxesActor.ShaftType(shaft_type)
if shaft_type == AxesActor.ShaftType.CYLINDER:
self.SetShaftTypeToCylinder()
elif shaft_type == AxesActor.ShaftType.LINE:
self.SetShaftTypeToLine()
@property
def tip_type(self) -> TipType: # numpydoc ignore=RT01
"""Return or set the shaft type.
Can be either a cone(0) or a sphere(1).
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.tip_type = axes.axes_actor.TipType.SPHERE
>>> axes.axes_actor.tip_type
<TipType.SPHERE: 1>
"""
return AxesActor.TipType(self.GetTipType())
@tip_type.setter
def tip_type(self, tip_type: TipType | int):
tip_type = AxesActor.TipType(tip_type)
if tip_type == AxesActor.TipType.CONE:
self.SetTipTypeToCone()
elif tip_type == AxesActor.TipType.SPHERE:
self.SetTipTypeToSphere()
@property
def labels(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
"""Return or set the axes labels.
This property may be used as an alternative to using :attr:`~x_axis_label`,
:attr:`~y_axis_label`, and :attr:`~z_axis_label` separately.
.. versionadded:: 0.44.0
Examples
--------
>>> import pyvista as pv
>>> axes_actor = pv.AxesActor()
>>> axes_actor.labels = ['X Axis', 'Y Axis', 'Z Axis']
>>> axes_actor.labels
('X Axis', 'Y Axis', 'Z Axis')
"""
return self.x_label, self.y_label, self.z_label
@labels.setter
def labels(self, labels: list[str] | tuple[str]):
if not isinstance(labels, (list, tuple)):
msg = f'Labels must be a list or tuple. Got {labels} instead.' # type: ignore[unreachable]
raise TypeError(msg)
if len(labels) != 3:
msg = f'Labels must be a list or tuple with three items. Got {labels} instead.'
raise ValueError(msg)
self.x_label = labels[0]
self.y_label = labels[1]
self.z_label = labels[2]
@property
def x_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the x-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`x_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `x_axis_label` is deprecated. Use `x_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetXAxisLabelText() # pragma: no cover
@x_axis_label.setter
def x_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `x_axis_label` is deprecated. Use `x_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetXAxisLabelText(label) # pragma: no cover
@property
def x_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the x-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.x_label = 'This axis'
>>> axes.axes_actor.x_label
'This axis'
"""
return self.GetXAxisLabelText()
@x_label.setter
def x_label(self, label: str):
self.SetXAxisLabelText(label)
@property
def y_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the y-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`y_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `y_axis_label` is deprecated. Use `y_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetYAxisLabelText() # pragma: no cover
@y_axis_label.setter
def y_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `y_axis_label` is deprecated. Use `y_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetYAxisLabelText(label) # pragma: no cover
@property
def y_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the y-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.y_label = 'This axis'
>>> axes.axes_actor.y_label
'This axis'
"""
return self.GetYAxisLabelText()
@y_label.setter
def y_label(self, label: str):
self.SetYAxisLabelText(label)
@property
def z_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the z-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`z_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `z_axis_label` is deprecated. Use `z_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetZAxisLabelText() # pragma: no cover
@z_axis_label.setter
def z_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `z_axis_label` is deprecated. Use `z_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetZAxisLabelText(label) # pragma: no cover
@property
def z_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the z-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.z_label = 'This axis'
>>> axes.axes_actor.z_label
'This axis'
"""
return self.GetZAxisLabelText()
@z_label.setter
def z_label(self, label: str):
self.SetZAxisLabelText(label)
@property
def x_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the x-axis shaft."""
return ActorProperties(self.GetXAxisShaftProperty())
@property
def y_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the y-axis shaft."""
return ActorProperties(self.GetYAxisShaftProperty())
@property
def z_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the z-axis shaft."""
return ActorProperties(self.GetZAxisShaftProperty())
@property
def x_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the x-axis tip."""
return ActorProperties(self.GetXAxisTipProperty())
@x_axis_tip_properties.setter
def x_axis_tip_properties(self, properties: ActorProperties):
self.x_axis_tip_properties = properties
@property
def y_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the y-axis tip."""
return ActorProperties(self.GetYAxisTipProperty())
@y_axis_tip_properties.setter
def y_axis_tip_properties(self, properties: ActorProperties):
self.y_axis_tip_properties = properties
@property
def z_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the z-axis tip."""
return ActorProperties(self.GetZAxisTipProperty())
@z_axis_tip_properties.setter
def z_axis_tip_properties(self, properties: ActorProperties):
self.z_axis_tip_properties = properties
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
"""Contains the BackgroundRenderer class."""
from __future__ import annotations
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from .renderer import Renderer
class BackgroundRenderer(Renderer):
"""BackgroundRenderer for visualizing a background image.
Parameters
----------
parent : Renderer
The parent renderer for the background renderer.
image_path : str
Path to the image to use as a background.
scale : float, default: 1
Scaling factor for the background image.
view_port : tuple[float], optional
Viewport for the background renderer.
"""
@_deprecate_positional_args(allowed=['parent', 'image_path'])
def __init__( # noqa: PLR0917
self, parent, image_path, scale=1, view_port=None
):
"""Initialize BackgroundRenderer with an image."""
# avoiding circular import
from . import _vtk # noqa: PLC0415
# read the image first as we don't need to create a render if
# the image path is invalid
image_data = pyvista.read(image_path)
super().__init__(parent, border=False)
self.SetLayer(0)
self.InteractiveOff()
self.SetBackground(self.parent.renderer.GetBackground())
self._scale = scale
self._modified_observer = None
self._prior_window_size = None
if view_port is not None:
self.viewport = view_port
# create image actor
image_actor = _vtk.vtkImageActor()
image_actor.SetInputData(image_data)
self.add_actor(image_actor, name='background')
self.camera.enable_parallel_projection()
self.reset_camera() # necessary to get first render
self.resize()
def resize(self, *args): # noqa: ARG002
"""Resize a background renderer.
Parameters
----------
*args : tuple
Ignored arguments.
"""
if self.parent is None: # when deleted
return
if self.parent.render_window is None: # BasePlotter
return
if self._prior_window_size != self.parent.window_size:
self._prior_window_size = self.parent.window_size
actor = self._actors['background']
image_data = actor.GetInput()
origin = image_data.GetOrigin()
extent = image_data.GetExtent()
spacing = image_data.GetSpacing()
xc = origin[0] + 0.5 * (extent[0] + extent[1]) * spacing[0]
yc = origin[1] + 0.5 * (extent[2] + extent[3]) * spacing[1]
yd = (extent[3] - extent[2] + 1) * spacing[1]
dist = self.camera.distance
# make the longest dimensions match the plotting window
img_dim = np.array(image_data.dimensions[:2])
self.camera._focus = np.array([xc, yc, 0.0])
self.camera.position = np.array([xc, yc, dist])
ratio = img_dim / np.array(self.parent.window_size)
scale_value = 1
if ratio.max() > 1:
# images are not scaled if larger than the window
scale_value = ratio.max()
if self._scale is not None:
scale_value /= self._scale
self.camera.parallel_scale = 0.5 * yd / self._scale
@@ -0,0 +1,912 @@
"""Module containing pyvista implementation of :vtk:`vtkCamera`."""
from __future__ import annotations
from pathlib import Path
from weakref import proxy
import xml.dom.minidom as md
from xml.etree import ElementTree as ET
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .helpers import view_vectors
class Camera(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkCamera):
"""PyVista wrapper for the VTK Camera class.
Parameters
----------
renderer : pyvista.Renderer, optional
Renderer to attach the camera to.
Examples
--------
Create a camera at the pyvista module level.
>>> import pyvista as pv
>>> camera = pv.Camera()
Access the active camera of a plotter and get the position of the
camera.
>>> pl = pv.Plotter()
>>> pl.camera.position
(1.0, 1.0, 1.0)
"""
def __init__(self, renderer=None):
"""Initialize a new camera descriptor."""
self._parallel_projection = False
self._elevation = 0.0
self._azimuth = 0.0
self._is_set = False
self._focus = None # Used by BackgroundRenderer
if renderer:
if not isinstance(renderer, pyvista.Renderer):
msg = 'Camera only accepts a pyvista.Renderer or None as the ``renderer`` argument'
raise TypeError(msg)
self._renderer = proxy(renderer)
else:
self._renderer = None # type: ignore[assignment]
def __eq__(self, other) -> bool:
"""Compare whether the relevant attributes of two cameras are equal."""
# attributes which are native python types and thus implement __eq__
native_attrs = [
'position',
'focal_point',
'parallel_projection',
'distance',
'thickness',
'parallel_scale',
'clipping_range',
'view_angle',
'roll',
]
for attr in native_attrs:
if getattr(self, attr) != getattr(other, attr):
return False
this_trans = self.model_transform_matrix
that_trans = other.model_transform_matrix
trans_count = sum(1 for trans in [this_trans, that_trans] if trans is not None)
if trans_count == 1:
# either but not both are None
return False
return not (trans_count == 2 and not np.array_equal(this_trans, that_trans))
__hash__ = None # type: ignore[assignment] # https://github.com/pyvista/pyvista/pull/7671
def __repr__(self):
"""Print a repr specifying the id of the camera and its camera type."""
repr_str = f'{self.__class__.__name__} ({hex(id(self))})'
repr_str += f'\n Position: {self.position}'
repr_str += f'\n Focal Point: {self.focal_point}'
repr_str += f'\n Parallel Projection: {self.parallel_projection}'
repr_str += f'\n Distance: {self.distance}'
repr_str += f'\n Thickness: {self.thickness}'
repr_str += f'\n Parallel Scale: {self.parallel_scale}'
repr_str += f'\n Clipping Range: {self.clipping_range}'
repr_str += f'\n View Angle: {self.view_angle}'
repr_str += f'\n Roll: {self.roll}'
return repr_str
def __str__(self):
"""Return the object string representation."""
return self.__repr__()
def __del__(self):
"""Delete the camera."""
self.RemoveAllObservers()
@property
def is_set(self) -> bool: # numpydoc ignore=RT01
"""Get or set whether this camera has been configured."""
return self._is_set
@is_set.setter
def is_set(self, value: bool):
self._is_set = bool(value)
@classmethod
def from_paraview_pvcc(cls, filename: str | Path) -> Camera:
"""Load a Paraview camera file (.pvcc extension).
Returns a pyvista.Camera object for which attributes has been read
from the filename argument.
Parameters
----------
filename : str or pathlib.Path
Path to Paraview camera file (.pvcc).
Returns
-------
pyvista.Camera
Camera from the camera file.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera = pv.Camera.from_paraview_pvcc('camera.pvcc') # doctest:+SKIP
>>> pl.camera.position
(1.0, 1.0, 1.0)
"""
to_find = {
'CameraPosition': ('position', float),
'CameraFocalPoint': ('focal_point', float),
'CameraViewAngle': ('view_angle', float),
'CameraViewUp': ('up', float),
'CameraParallelProjection': ('parallel_projection', int),
'CameraParallelScale': ('parallel_scale', float),
}
camera = cls()
tree = ET.parse(filename)
root = tree.getroot()[0]
for element in root:
attrib = element.attrib
attrib_name = attrib['name']
if attrib_name in to_find:
name, typ = to_find[attrib_name]
nelems = int(attrib['number_of_elements'])
# Set the camera attributes
if nelems == 3:
values = [typ(e.attrib['value']) for e in element]
setattr(camera, name, values)
elif nelems == 1:
# Special case for bool since bool("0") returns True.
# So first convert to int from `to_find` and then apply bool
if 'name' in element[-1].attrib and element[-1].attrib['name'] == 'bool':
val = bool(typ(element[0].attrib['value']))
else:
val = typ(element[0].attrib['value'])
setattr(camera, name, val)
camera.is_set = True
return camera
def to_paraview_pvcc(self, filename: str | Path):
"""Write the camera parameters to a Paraview camera file (.pvcc extension).
Parameters
----------
filename : str or pathlib.Path
Path to Paraview camera file (.pvcc).
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.to_paraview_pvcc('camera.pvcc') # doctest:+SKIP
"""
root = ET.Element('PVCameraConfiguration')
root.attrib['description'] = 'ParaView camera configuration'
root.attrib['version'] = '1.0'
dico = dict(group='views', type='RenderView', id='0', servers='21')
proxy = ET.SubElement(root, 'Proxy', dico)
# Add tuples
to_find = {
'CameraPosition': 'position',
'CameraFocalPoint': 'focal_point',
'CameraViewUp': 'up',
}
for name, attr in to_find.items():
e = ET.SubElement(
proxy,
'Property',
dict(name=name, id=f'0.{name}', number_of_elements='3'),
)
for i in range(3):
tmp = ET.Element('Element')
tmp.attrib['index'] = str(i)
tmp.attrib['value'] = str(getattr(self, attr)[i])
e.append(tmp)
# Add single values
to_find = {
'CameraViewAngle': 'view_angle',
'CameraParallelScale': 'parallel_scale',
'CameraParallelProjection': 'parallel_projection',
}
for name, attr in to_find.items():
e = ET.SubElement(
proxy,
'Property',
dict(name=name, id=f'0.{name}', number_of_elements='1'),
)
tmp = ET.Element('Element')
tmp.attrib['index'] = '0'
val = getattr(self, attr)
if not isinstance(val, bool):
tmp.attrib['value'] = str(val)
e.append(tmp)
else:
tmp.attrib['value'] = '1' if val else '0'
e.append(tmp)
e.append(ET.Element('Domain', dict(name='bool', id=f'0.{name}.bool')))
xmlstr = ET.tostring(root).decode()
newxml = md.parseString(xmlstr)
with Path(filename).open('w') as outfile:
outfile.write(newxml.toprettyxml(indent='\t', newl='\n'))
@property
def position(self): # numpydoc ignore=RT01
"""Return or set the position of the camera in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.position
(1.0, 1.0, 1.0)
>>> pl.camera.position = (2.0, 1.0, 1.0)
>>> pl.camera.position
(2.0, 1.0, 1.0)
"""
return self.GetPosition()
@position.setter
def position(self, value):
self.SetPosition(value)
self._elevation = 0.0
self._azimuth = 0.0
if self._renderer: # type: ignore[truthy-bool]
self.reset_clipping_range()
self.is_set = True
def reset_clipping_range(self):
"""Reset the camera clipping range based on the bounds of the visible actors.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.clipping_range = (1, 2)
>>> pl.camera.reset_clipping_range() # doctest:+SKIP
(0.0039213485598532955, 3.9213485598532953)
"""
if self._renderer is None:
msg = 'Camera is must be associated with a renderer to reset its clipping range.' # type: ignore[unreachable]
raise AttributeError(msg)
self._renderer.reset_camera_clipping_range()
@property
def focal_point(self): # numpydoc ignore=RT01
"""Location of the camera's focus in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.focal_point
(0.0, 0.0, 0.0)
>>> pl.camera.focal_point = (2.0, 0.0, 0.0)
>>> pl.camera.focal_point
(2.0, 0.0, 0.0)
"""
return self.GetFocalPoint()
@focal_point.setter
def focal_point(self, point):
self.SetFocalPoint(point)
self.is_set = True
@property
def model_transform_matrix(self): # numpydoc ignore=RT01
"""Return or set the camera's model transformation matrix.
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> pl = pv.Plotter()
>>> pl.camera.model_transform_matrix
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
>>> pl.camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 0.5],
... ]
... )
>>>
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 0.5]])
"""
vtk_matrix = self.GetModelTransformMatrix()
matrix = np.empty((4, 4))
vtk_matrix.DeepCopy(matrix.ravel(), vtk_matrix)
return matrix
@model_transform_matrix.setter
def model_transform_matrix(self, matrix):
vtk_matrix = _vtk.vtkMatrix4x4()
vtk_matrix.DeepCopy(matrix.ravel())
self.SetModelTransformMatrix(vtk_matrix)
@property
def distance(self): # numpydoc ignore=RT01
"""Return or set the distance of the focal point from the camera.
Notes
-----
Setting the distance keeps the camera fixed and moves the focal point.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.distance
1.73205
>>> pl.camera.distance = 2.0
>>> pl.camera.distance
2.0
"""
return self.GetDistance()
@distance.setter
def distance(self, distance):
self.SetDistance(distance)
self.is_set = True
@property
def thickness(self): # numpydoc ignore=RT01
"""Return or set the distance between clipping planes.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.thickness
1000.0
>>> pl.camera.thickness = 100
>>> pl.camera.thickness
100.0
"""
return self.GetThickness()
@thickness.setter
def thickness(self, length):
self.SetThickness(length)
@property
def parallel_scale(self): # numpydoc ignore=RT01
"""Return or set the scaling used for a parallel projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.parallel_scale
1.0
>>> pl.camera.parallel_scale = 2.0
>>> pl.camera.parallel_scale
2.0
"""
return self.GetParallelScale()
@parallel_scale.setter
def parallel_scale(self, scale):
self.SetParallelScale(scale)
def zoom(self, value):
"""Set the zoom of the camera.
In perspective mode, decrease the view angle by the specified
factor.
In parallel mode, decrease the parallel scale by the specified
factor. A value greater than 1 is a zoom-in, a value less than
1 is a zoom-out.
Parameters
----------
value : float or str
Zoom of the camera. If a float, must be greater than 0. Otherwise,
if a string, must be ``"tight"``. If tight, the plot will be zoomed
such that the actors fill the entire viewport.
Examples
--------
Show the Default zoom.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom(1.0)
>>> pl.show()
Show 2x zoom.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom(2.0)
>>> pl.show()
Zoom so the actor fills the entire render window.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
if isinstance(value, str):
if value != 'tight':
msg = 'If a string, ``zoom`` can only be "tight"'
raise ValueError(msg)
self.tight()
return
self.Zoom(value)
self.is_set = True
@property
def up(self): # numpydoc ignore=RT01
"""Return or set the "up" of the camera.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.up
(0.0, 0.0, 1.0)
>>> pl.camera.up = (0.410018, 0.217989, 0.885644)
>>> pl.camera.up
(0.410018, 0.217989, 0.885644)
"""
return self.GetViewUp()
@up.setter
def up(self, vector):
self.SetViewUp(vector)
self.is_set = True
def enable_parallel_projection(self):
"""Enable parallel projection.
The camera will have a parallel projection. Parallel
projection is often useful when viewing images or 2D datasets,
but will look odd when viewing 3D datasets.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.demos.orientation_plotter()
>>> pl.enable_parallel_projection()
>>> pl.show()
"""
self._parallel_projection = True
self.SetParallelProjection(True)
def disable_parallel_projection(self):
"""Disable the use of parallel projection.
This is default behavior.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.demos.orientation_plotter()
>>> pl.disable_parallel_projection()
>>> pl.show()
"""
self._parallel_projection = False
self.SetParallelProjection(False)
@property
def parallel_projection(self): # numpydoc ignore=RT01
"""Return the state of the parallel projection.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.Plotter()
>>> pl.disable_parallel_projection()
>>> pl.parallel_projection
False
"""
return self._parallel_projection
@parallel_projection.setter
def parallel_projection(self, state):
if state:
self.enable_parallel_projection()
else:
self.disable_parallel_projection()
@property
def clipping_range(self): # numpydoc ignore=RT01
"""Return or set the location of the clipping planes.
Clipping planes are the near and far clipping planes along
the direction of projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.clipping_range
(0.01, 1000.01)
>>> pl.camera.clipping_range = (1, 10)
>>> pl.camera.clipping_range
(1.0, 10.0)
"""
return self.GetClippingRange()
@clipping_range.setter
def clipping_range(self, points):
if points[0] > points[1]:
msg = 'Near point must be lower than the far point.'
raise ValueError(msg)
self.SetClippingRange(points[0], points[1])
@property
def view_angle(self): # numpydoc ignore=RT01
"""Return or set the camera view angle.
Examples
--------
>>> import pyvista as pv
>>> plotter = pv.Plotter()
>>> plotter.camera.view_angle
30.0
>>> plotter.camera.view_angle = 60.0
>>> plotter.camera.view_angle
60.0
"""
return self.GetViewAngle()
@view_angle.setter
def view_angle(self, value):
self.SetViewAngle(value)
@property
def direction(self): # numpydoc ignore=RT01
"""Vector from the camera position to the focal point.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.direction # doctest:+SKIP
(-0.5773502691896257, -0.5773502691896257, -0.5773502691896257)
"""
return self.GetDirectionOfProjection()
def view_frustum(self, aspect=1.0):
"""Get the view frustum.
Parameters
----------
aspect : float, default: 1.0
The aspect of the viewport to compute the planes.
Returns
-------
pyvista.PolyData
View frustum.
Examples
--------
>>> import pyvista as pv
>>> plotter = pv.Plotter()
>>> frustum = plotter.camera.view_frustum(1.0)
>>> frustum.n_points
8
>>> frustum.n_cells
6
"""
frustum_planes = [0] * 24
self.GetFrustumPlanes(aspect, frustum_planes) # type: ignore[arg-type]
planes = _vtk.vtkPlanes()
planes.SetFrustumPlanes(frustum_planes) # type: ignore[arg-type]
frustum_source = _vtk.vtkFrustumSource()
frustum_source.ShowLinesOff()
frustum_source.SetPlanes(planes)
frustum_source.Update()
return pyvista.wrap(frustum_source.GetOutput())
@property
def roll(self): # numpydoc ignore=RT01
"""Return or set the roll of the camera about the direction of projection.
This will spin the camera about its axis.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.roll
-120.00000000000001
>>> pl.camera.roll = 45.0
>>> pl.camera.roll
45.0
"""
return self.GetRoll()
@roll.setter
def roll(self, angle):
self.SetRoll(angle)
self.is_set = True
@property
def elevation(self): # numpydoc ignore=RT01
"""Return or set the vertical rotation of the scene.
Rotate the camera about the cross product of the negative of
the direction of projection and the view up vector, using the
focal point as the center of rotation.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.elevation
0.0
>>> pl.camera.elevation = 45.0
>>> pl.camera.elevation
45.0
"""
return self._elevation
@elevation.setter
def elevation(self, angle):
if self._elevation:
self.Elevation(-self._elevation)
self._elevation = angle
self.Elevation(angle)
self.is_set = True
@property
def azimuth(self): # numpydoc ignore=RT01
"""Return or set the azimuth of the camera.
Rotate the camera about the view up vector centered at the
focal point. Note that the view up vector is whatever was set
via SetViewUp, and is not necessarily perpendicular to the
direction of projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.azimuth
0.0
>>> pl.camera.azimuth = 45.0
>>> pl.camera.azimuth
45.0
"""
return self._azimuth
@azimuth.setter
def azimuth(self, angle):
if self._azimuth:
self.Azimuth(-self._azimuth)
self._azimuth = angle
self.Azimuth(angle)
self.is_set = True
def copy(self):
"""Return a deep copy of the camera.
Returns
-------
pyvista.Camera
Deep copy of the camera.
Examples
--------
Create a camera and check that it shares a transformation
matrix with its shallow copy.
>>> import pyvista as pv
>>> import numpy as np
>>> camera = pv.Camera()
>>> camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 1.0],
... ]
... )
>>> copied_camera = camera.copy()
>>> copied_camera == camera
True
>>> camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 0.5],
... ]
... )
>>> copied_camera == camera
False
"""
immutable_attrs = [
'position',
'focal_point',
'model_transform_matrix',
'distance',
'thickness',
'parallel_scale',
'up',
'clipping_range',
'view_angle',
'roll',
'parallel_projection',
'is_set',
]
new_camera = Camera()
for attr in immutable_attrs:
value = getattr(self, attr)
setattr(new_camera, attr, value)
return new_camera
@_deprecate_positional_args
def tight( # noqa: PLR0917
self,
padding=0.0,
adjust_render_window: bool = True, # noqa: FBT001, FBT002
view='xy',
negative: bool = False, # noqa: FBT001, FBT002
):
"""Adjust the camera position so that the actors fill the entire renderer.
The camera view direction is reoriented to be normal to the ``view``
plane. When ``negative=False``, The first letter of ``view`` refers
to the axis that points to the right. The second letter of ``view``
refers to axis that points up. When ``negative=True``, the first
letter refers to the axis that points left. The up direction is
unchanged.
Parallel projection is enabled when using this function.
Parameters
----------
padding : float, default: 0.0
Additional padding around the actor(s). This is effectively a zoom,
where a value of 0.01 results in a zoom out of 1%.
adjust_render_window : bool, default: True
Adjust the size of the render window as to match the dimensions of
the visible actors.
view : {'xy', 'yx', 'xz', 'zx', 'yz', 'zy'}, default: 'xy'
Plane to which the view is oriented.
negative : bool, default: False
Whether to view in opposite direction.
Notes
-----
This resets the view direction to look at a plane with parallel projection.
Examples
--------
Display the puppy image with a tight view.
>>> import pyvista as pv
>>> from pyvista import examples
>>> puppy = examples.download_puppy()
>>> pl = pv.Plotter(border=True, border_width=5)
>>> _ = pl.add_mesh(puppy, rgb=True)
>>> pl.camera.tight()
>>> pl.show()
Set the background to blue use a 5% padding around the image.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(puppy, rgb=True)
>>> pl.background_color = 'b'
>>> pl.camera.tight(padding=0.05)
>>> pl.show()
"""
# Inspired by vedo resetCamera. Thanks @marcomusy.
x0, x1, y0, y1, z0, z1 = self._renderer.bounds
self.enable_parallel_projection()
self._renderer.ComputeAspect()
aspect = self._renderer.GetAspect()
position0 = np.array([x0, y0, z0])
position1 = np.array([x1, y1, z1])
objects_size = position1 - position0
position = position0 + objects_size / 2
direction, viewup = view_vectors(view, negative=negative)
horizontal = np.cross(direction, viewup)
vert_dist = abs(objects_size @ viewup)
horiz_dist = abs(objects_size @ horizontal)
# set focal point to objects' center
# offset camera position from objects center by dist in opposite of viewing direction
# (actual distance doesn't matter due to parallel projection)
dist = 1
camera_position = position + dist * direction
self.SetViewUp(*viewup)
self.SetPosition(*camera_position)
self.SetFocalPoint(*position)
ps = max(horiz_dist / aspect[0], vert_dist) / 2
self.parallel_scale = ps * (1 + padding)
self._renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)
if adjust_render_window:
ren_win = self._renderer.GetRenderWindow()
size = list(ren_win.GetSize())
size_ratio = size[0] / size[1]
tight_ratio = horiz_dist / vert_dist
resize_ratio = tight_ratio / size_ratio
if resize_ratio < 1:
size[0] = round(size[0] * resize_ratio)
else:
size[1] = round(size[1] / resize_ratio)
ren_win.SetSize(size)
# simply call tight again to reset the parallel scale due to the
# resized window
self.tight(padding=padding, adjust_render_window=False, view=view, negative=negative)
self.is_set = True
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,880 @@
"""Module containing composite data mapper."""
from __future__ import annotations
from itertools import cycle
import sys
from typing import TYPE_CHECKING
import weakref
import numpy as np
import pyvista
from pyvista import vtk_version_info
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.arrays import convert_array
from pyvista.core.utilities.arrays import convert_string_array
from pyvista.core.utilities.misc import _check_range
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .colors import Color
from .colors import get_cycler
from .mapper import _BaseMapper
if TYPE_CHECKING:
from collections.abc import Sequence
import cycler
from ._typing import ColorLike
class BlockAttributes(_NoNewAttrMixin):
"""Block attributes used to set the attributes of a block.
Parameters
----------
block : pyvista.DataObject
PyVista data object.
attr : pyvista.plotting.composite_mapper.CompositeAttributes
Parent attributes.
Notes
-----
This class employs VTK's flat indexing and allows for accessing both
the blocks of a composite dataset as well as the entire composite
dataset. If there is only one composite dataset, ``A``, which contains
datasets ``[b, c]``, the indexing would be ``[A, b, c]``.
If there are two composite datasets ``[B, C]`` in one composite
dataset, ``A``, each of which containing three additional datasets
``[d, e, f]``, and ``[g, h, i]``, respectively, then the head node,
``A``, would be the zero index, followed by the first child, ``B``,
followed by all the children of ``B``, ``[d, e, f]``. In data
structures, this flat indexing would be known as "Depth-first search"
and the entire indexing would be::
[A, B, d, e, f, C, g, h, i]
Note how the composite datasets themselves are capitalized and are
accessible in the flat indexing, and not just the datasets.
Examples
--------
Add a sphere and a cube as a multiblock dataset to a plotter and then
change the visibility and color of the blocks. Note how the index of the
cube is ``1`` as the index of the entire multiblock is ``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.block_attr[1].color = 'b'
>>> mapper.block_attr[1].opacity = 0.1
>>> mapper.block_attr[1]
Composite Block Addr=... Attributes
Visible: None
Opacity: 0.1
Color: Color(name='blue', hex='#0000ffff', opacity=255)
Pickable None
>>> pl.show()
"""
def __init__(self, block, attr):
"""Initialize the block attributes class."""
self._block = block
self.__attr = weakref.ref(attr)
@property
def _attr(self):
"""Return the CompositeAttributes."""
return self.__attr()
@property
def _has_color(self):
"""Return if a block has its color set."""
return self._attr.HasBlockColor(self._block)
@property
def _has_visibility(self):
"""Return if a block has its visibility set."""
return self._attr.HasBlockVisibility(self._block)
@property
def _has_opacity(self):
"""Return if a block has its opacity set."""
return self._attr.HasBlockOpacity(self._block)
@property
def _has_pickable(self):
"""Return if a block has its pickability set."""
return self._attr.HasBlockPickability(self._block)
@property
def color(self): # numpydoc ignore=RT01
"""Get or set the color of a block.
Examples
--------
Set the colors of a composite dataset to red and blue.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[1].color = 'r'
>>> mapper.block_attr[2].color = 'b'
>>> pl.show()
"""
if not self._has_color:
return None
return Color(tuple(self._attr.GetBlockColor(self._block)))
@color.setter
def color(self, new_color):
if new_color is None:
self._attr.RemoveBlockColor(self._block)
self._attr.Modified()
return
self._attr.SetBlockColor(self._block, Color(new_color).float_rgb)
@property
def visible(self) -> bool | None: # numpydoc ignore=RT01
"""Get or set the visibility of a block.
Examples
--------
Hide the first block of a composite dataset.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[1].visible = False
>>> pl.show()
"""
if not self._has_visibility:
return None
return self._attr.GetBlockVisibility(self._block)
@visible.setter
def visible(self, new_visible: bool | None):
if new_visible is None:
self._attr.RemoveBlockVisibility(self._block)
self._attr.Modified()
return
self._attr.SetBlockVisibility(self._block, new_visible)
@property
def opacity(self) -> float | None: # numpydoc ignore=RT01
"""Get or set the opacity of a block.
If opacity has not been set this will be ``None``.
Warnings
--------
VTK 9.0.3 has a bug where changing the opacity to less than 1.0 also
changes the edge visibility on the block that is partially transparent.
Examples
--------
Change the opacity of the second block of the dataset.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[2].opacity = 0.5
>>> pl.show()
"""
if not self._has_opacity:
return None
return self._attr.GetBlockOpacity(self._block)
@opacity.setter
def opacity(self, new_opacity: float | None):
if new_opacity is None:
self._attr.RemoveBlockOpacity(self._block)
self._attr.Modified()
return
_check_range(new_opacity, (0, 1), 'opacity')
self._attr.SetBlockOpacity(self._block, new_opacity)
@property
def pickable(self) -> bool | None: # numpydoc ignore=RT01
"""Get or set the pickability of a block.
Examples
--------
Make the cube of a multiblock dataset pickable and the sphere unpickable.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[1].pickable = True
>>> mapper.block_attr[2].pickable = False
>>> pl.close()
See :ref:`composite_picking_example` for a full example using block
picking.
"""
if not self._has_pickable:
return None
return self._attr.GetBlockPickability(self._block)
@pickable.setter
def pickable(self, new_pickable: bool | None):
if new_pickable is None:
self._attr.RemoveBlockPickability(self._block)
self._attr.Modified()
return
self._attr.SetBlockPickability(self._block, new_pickable)
def __repr__(self):
"""Representation of block properties."""
return '\n'.join(
[
f'Composite Block {self._block.memory_address} Attributes',
f'Visible: {self.visible}',
f'Opacity: {self.opacity}',
f'Color: {self.color}',
f'Pickable {self.pickable}',
],
)
class CompositeAttributes(
_NoNewAttrMixin,
_vtk.DisableVtkSnakeCase,
_vtk.vtkCompositeDataDisplayAttributes,
):
"""Block attributes.
Parameters
----------
mapper : pyvista.plotting.composite_mapper.CompositePolyDataMapper
Parent mapper.
dataset : pyvista.MultiBlock
Multiblock dataset.
Notes
-----
This class employs VTK's flat indexing and allows for accessing both
the blocks of a composite dataset as well as the entire composite
dataset. If there is only one composite dataset, ``A``, which contains
datasets ``[b, c]``, the indexing would be ``[A, b, c]``.
If there are two composite datasets ``[B, C]`` in one composite
dataset, ``A``, each of which containing three additional datasets
``[d, e, f]``, and ``[g, h, i]``, respectively, then the head node,
``A``, would be the zero index, followed by the first child, ``B``,
followed by all the children of ``B``, ``[d, e, f]``. In data
structures, this flat indexing would be known as "Depth-first search"
and the entire indexing would be::
[A, B, d, e, f, C, g, h, i]
Note how the composite datasets themselves are capitalized and are
accessible in the flat indexing, and not just the datasets.
Examples
--------
Add a sphere and a cube as a multiblock dataset to a plotter and then
change the visibility and color of the blocks. Note how the index of the
cube is ``1`` as the index of the entire multiblock is ``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.block_attr[1].color = 'b'
>>> mapper.block_attr[1].opacity = 0.1
>>> mapper.block_attr[1]
Composite Block Addr=... Attributes
Visible: None
Opacity: 0.1
Color: Color(name='blue', hex='#0000ffff', opacity=255)
Pickable None
>>> pl.show()
"""
def __init__(self, mapper, dataset):
"""Initialize CompositeAttributes."""
super().__init__()
mapper.SetCompositeDataDisplayAttributes(self)
self._dataset = dataset
def reset_visibilities(self):
"""Reset the visibility of all blocks.
Examples
--------
Hide the first block of a composite dataset and then show all by
resetting visibilities.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[1].visible = False
>>> mapper.block_attr.reset_visibilities()
>>> pl.show()
"""
self.RemoveBlockVisibilities()
def reset_pickabilities(self):
"""Reset the pickability of all blocks.
Examples
--------
Make the cube of a multiblock dataset pickable and the sphere
unpickable, then reset it.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[1].pickable = True
>>> mapper.block_attr[2].pickable = False
>>> mapper.block_attr.reset_pickabilities()
>>> [
... mapper.block_attr[1].pickable,
... mapper.block_attr[2].pickable,
... ]
[None, None]
>>> pl.close()
"""
self.RemoveBlockPickabilities()
def reset_colors(self):
"""Reset the color of all blocks.
Examples
--------
Set individual block colors and then reset them.
>>> import pyvista as pv
>>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))])
>>> pl = pv.Plotter()
>>> actor, mapper = pl.add_composite(dataset, color='w')
>>> mapper.block_attr[1].color = 'r'
>>> mapper.block_attr[2].color = 'b'
>>> mapper.block_attr.reset_colors()
>>> pl.show()
"""
self.RemoveBlockColors()
def reset_opacities(self):
"""Reset the opacities of all blocks.
Examples
--------
Change the opacity of the second block of the dataset then reset all
opacities.
Note how the zero index is the entire multiblock, so we have to add 1
to our indexing to access the right block.
>>> 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.block_attr[2].opacity = 0.5
>>> mapper.block_attr.reset_opacities()
>>> pl.show()
"""
self.RemoveBlockOpacities()
def get_block(self, index):
"""Return a block by its flat index.
Parameters
----------
index : int
Flat index of the block to retrieve.
Returns
-------
pyvista.DataObject
PyVista data object.
Notes
-----
This method employs VTK's flat indexing and allows for accessing both
the blocks of a composite dataset as well as the entire composite
dataset. If there is only one composite dataset, ``A``, which contains
datasets ``[b, c]``, the indexing would be ``[A, b, c]``.
If there are two composite datasets ``[B, C]`` in one composite
dataset, ``A``, each of which containing three additional datasets
``[d, e, f]``, and ``[g, h, i]``, respectively, then the head node,
``A``, would be the zero index, followed by the first child, ``B``,
followed by all the children of ``B``, ``[d, e, f]``. In data
structures, this flat indexing would be known as "Depth-first search"
and the entire indexing would be::
[A, B, d, e, f, C, g, h, i]
Note how the composite datasets themselves are capitalized and are
accessible in the flat indexing, and not just the datasets.
Examples
--------
Add a composite dataset to a plotter and access its block attributes.
Note how the zero index is the entire multiblock and you can use ``1``
and ``2`` to access the individual sub-blocks.
>>> 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.block_attr.get_block(0)
MultiBlock (...)
N Blocks: 2
X Bounds: -5.000e-01, 5.000e-01
Y Bounds: -5.000e-01, 5.000e-01
Z Bounds: -5.000e-01, 1.500e+00
Note this is the same as using ``__getitem__``
>>> mapper.block_attr[0]
Composite Block Addr=... Attributes
Visible: None
Opacity: None
Color: None
Pickable None
"""
try:
if vtk_version_info <= (9, 0, 3): # pragma: no cover
vtk_ref = _vtk.reference(0) # needed for <=9.0.3
block = self.DataObjectFromIndex(index, self._dataset, vtk_ref) # type: ignore[arg-type]
else:
block = self.DataObjectFromIndex(index, self._dataset)
except OverflowError:
msg = f'Invalid block key: {index}'
raise KeyError(msg) from None
if block is None and index > len(self) - 1:
msg = f'index {index} is out of bounds. There are only {len(self)} blocks.'
raise KeyError(msg) from None
return block
def __getitem__(self, index):
"""Return a block attribute by its flat index."""
return BlockAttributes(self.get_block(index), self)
def __len__(self):
"""Return the number of blocks in this dataset."""
from pyvista import MultiBlock # avoid circular # noqa: PLC0415
# start with 1 as there is always a composite dataset and this is the
# root of the tree
cc = 1
for dataset in self._dataset:
if isinstance(dataset, MultiBlock):
cc += len(dataset) + 1 # include the block itself
else:
cc += 1
return cc
def __iter__(self):
"""Return an iterator of all the block attributes."""
for ii in range(len(self)):
yield self[ii]
class CompositePolyDataMapper(
_BaseMapper,
(
_vtk.vtkCompositePolyDataMapper # type: ignore[misc]
if vtk_version_info >= (9, 3)
else _vtk.vtkCompositePolyDataMapper2
),
):
"""Composite PolyData mapper.
Parameters
----------
dataset : pyvista.MultiBlock, optional
Multiblock dataset.
theme : pyvista.plotting.themes.Theme, optional
Plot-specific theme.
color_missing_with_nan : bool, optional
Color any missing values with the ``nan_color``. This is useful
when not all blocks of the composite dataset have the specified
``scalars``.
interpolate_before_map : bool, optional
Enabling makes for a smoother scalars display. Default is
``True``. When ``False``, OpenGL will interpolate the
mapped colors which can result is showing colors that are
not present in the color map.
"""
@_deprecate_positional_args(allowed=['dataset'])
def __init__( # noqa: PLR0917
self,
dataset=None,
theme=None,
color_missing_with_nan=None,
interpolate_before_map=None,
):
"""Initialize this composite mapper."""
super().__init__(theme=theme)
# this must be added to set the color, opacity, and visibility of
# individual blocks
self._attr = CompositeAttributes(self, dataset)
self.dataset = dataset
if color_missing_with_nan is not None:
self.color_missing_with_nan = color_missing_with_nan
if interpolate_before_map is not None:
self.interpolate_before_map = interpolate_before_map
self._orig_scalars_name: str | None = None
@property
def dataset(self) -> pyvista.MultiBlock: # numpydoc ignore=RT01
"""Return the composite dataset assigned to this mapper.
Examples
--------
>>> 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.dataset
MultiBlock (...)
N Blocks: 2
X Bounds: -5.000e-01, 5.000e-01
Y Bounds: -5.000e-01, 5.000e-01
Z Bounds: -5.000e-01, 1.500e+00
"""
return self._dataset
@dataset.setter
def dataset(self, obj: pyvista.MultiBlock):
self.SetInputDataObject(obj)
self._dataset = obj
self._attr._dataset = obj
@property
def block_attr(self) -> CompositeAttributes: # numpydoc ignore=RT01
"""Return the block attributes.
Notes
-----
``block_attr`` employs VTK's flat indexing and allows for accessing
both the blocks of a composite dataset as well as the entire composite
dataset. If there is only one composite dataset, ``A``, which contains
datasets ``[b, c]``, the indexing would be ``[A, b, c]``.
If there are two composite datasets ``[B, C]`` in one composite
dataset, ``A``, each of which containing three additional datasets
``[d, e, f]``, and ``[g, h, i]``, respectively, then the head node,
``A``, would be the zero index, followed by the first child, ``B``,
followed by all the children of ``B``, ``[d, e, f]``. In data
structures, this flat indexing would be known as "Depth-first search"
and the entire indexing would be::
[A, B, d, e, f, C, g, h, i]
Examples
--------
Add a sphere and a cube as a multiblock dataset to a plotter and then
change the visibility and color of the blocks.
>>> 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.block_attr[1].color = 'b'
>>> mapper.block_attr[1].opacity = 0.1
>>> mapper.block_attr[1]
Composite Block Addr=... Attributes
Visible: None
Opacity: 0.1
Color: Color(name='blue', hex='#0000ffff', opacity=255)
Pickable None
"""
return self._attr
@property
def color_missing_with_nan(self) -> bool: # numpydoc ignore=RT01
"""Color missing arrays with the NaN color.
Examples
--------
Enable coloring missing values with NaN.
>>> import pyvista as pv
>>> dataset = pv.MultiBlock([pv.Cube(), pv.Sphere(center=(0, 0, 1))])
>>> dataset[0].point_data['data'] = dataset[0].points[:, 2]
>>> pl = pv.Plotter()
>>> actor, mapper = pl.add_composite(
... dataset, scalars='data', show_scalar_bar=False
... )
>>> pv.global_theme.nan_color = 'r'
>>> mapper.color_missing_with_nan = True
>>> pl.show()
"""
return self.GetColorMissingArraysWithNanColor()
@color_missing_with_nan.setter
def color_missing_with_nan(self, value: bool):
self.SetColorMissingArraysWithNanColor(value)
def set_unique_colors(
self,
color_cycler: bool | str | cycler.Cycler[str, ColorLike] | Sequence[ColorLike] = True, # noqa: FBT001, FBT002
):
"""Set each block of the dataset to a unique color.
This uses ``matplotlib``'s color cycler by default.
When a custom color cycler, or a sequence of
color-like objects, is passed it sets the blocks
to the corresponding colors.
Parameters
----------
color_cycler : bool | str | cycler.Cycler | sequence[ColorLike]
The sequence of colors to cycle through,
if ``True``, uses matplotlib cycler.
Examples
--------
Set each block of the composite dataset to a unique color.
>>> 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.set_unique_colors()
>>> mapper.block_attr[1].color
Color(name='tab:orange', hex='#ff7f0eff', opacity=255)
>>> mapper.block_attr[2].color
Color(name='tab:green', hex='#2ca02cff', opacity=255)
>>> pl.show()
"""
self.scalar_visibility = False
if isinstance(color_cycler, bool):
colors = cycle(get_cycler('matplotlib'))
else:
colors = cycle(get_cycler(color_cycler))
for attr in self.block_attr:
attr.color = next(colors)['color']
@_deprecate_positional_args(allowed=['scalars_name'])
def set_scalars( # noqa: PLR0917
self,
scalars_name,
preference,
component,
annotations,
rgb,
scalar_bar_args,
n_colors,
nan_color,
above_color,
below_color,
clim,
cmap,
flip_scalars,
log_scale,
):
"""Set the scalars of the mapper.
Parameters
----------
scalars_name : str
Name of the scalars in the dataset. Must already exist in at least
of the blocks.
preference : str
For each block, when ``block.n_points == block.n_cells`` and
setting scalars, this parameter sets how the scalars will be mapped
to the mesh. Default ``'point'``, causes the scalars will be
associated with the mesh points. Can be either ``'point'`` or
``'cell'``.
component : int
Set component of vector valued scalars to plot. Must be
nonnegative, if supplied. If ``None``, the magnitude of
the vector is plotted.
annotations : dict
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.
rgb : bool
If the ``scalars_name`` corresponds to a 2 dimensional array, plot
those values as RGB(A) colors.
scalar_bar_args : dict
Dictionary of keyword arguments to pass when adding the
scalar bar to the scene. For options, see
:func:`pyvista.Plotter.add_scalar_bar`.
n_colors : int
Number of colors to use when displaying scalars.
nan_color : ColorLike
The color to use for all ``NaN`` values in the plotted
scalar array.
above_color : ColorLike
Solid color for values below the scalars range
(``clim``). This will automatically set the scalar bar
``above_label`` to ``'above'``.
below_color : ColorLike
Solid color for values below the scalars range
(``clim``). This will automatically set the scalar bar
``below_label`` to ``'below'``.
clim : Sequence
Color bar range for scalars. Defaults to minimum and
maximum of scalars array. Example: ``[-1, 2]``. ``rng``
is also an accepted alias for this.
cmap : str | list | 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.
are installed, their colormaps can be specified by name.
flip_scalars : bool
Flip direction of cmap. Most colormaps allow ``*_r``
suffix to do this as well.
log_scale : bool
Use log scale when mapping data to colors. Scalars less
than zero are mapped to the smallest representable
positive float.
Returns
-------
dict
Dictionary of scalar bar arguments.
"""
self._orig_scalars_name = scalars_name
field, scalars_name, dtype = self._dataset._activate_plotting_scalars(
scalars_name=scalars_name,
preference=preference,
component=component,
rgb=rgb,
)
self.scalar_visibility = True
if rgb:
self.color_mode = 'direct'
return scalar_bar_args
else:
self.scalar_map_mode = field.name.lower()
scalar_bar_args.setdefault('title', scalars_name)
if clim is None:
clim = self._dataset.get_data_range(scalars_name, allow_missing=True)
self.scalar_range = clim
if log_scale and clim[0] <= 0:
clim = [sys.float_info.min, clim[1]]
if isinstance(cmap, pyvista.LookupTable):
self.lookup_table = cmap
else:
if dtype == np.bool_:
cats = np.array([b'False', b'True'], dtype='|S5')
values = np.array([0, 1])
n_colors = 2
scalar_bar_args.setdefault('n_labels', 0)
self.lookup_table.SetAnnotations(convert_array(values), convert_string_array(cats))
clim = [-0.5, 1.5]
self.lookup_table.log_scale = log_scale
if isinstance(annotations, dict):
self.lookup_table.annotations = annotations
# self.lookup_table.SetNumberOfTableValues(n_colors)
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 cmap is None:
cmap = pyvista.global_theme.cmap if self._theme is None else self._theme.cmap
if cmap is not None:
self.lookup_table.apply_cmap(cmap, n_colors, flip=flip_scalars)
elif flip_scalars:
self.lookup_table.SetHueRange(0.0, 0.66667)
else:
self.lookup_table.SetHueRange(0.66667, 0.0)
return scalar_bar_args
@@ -0,0 +1,645 @@
"""Module containing the wrapping of CubeAxesActor."""
from __future__ import annotations
from collections.abc import MutableSequence
from typing import TYPE_CHECKING
from typing import cast
import warnings
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 convert_string_array
from pyvista.core.utilities.misc import _BoundsSizeMixin
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
if TYPE_CHECKING:
from pyvista.core._typing_core import VectorLike
@_deprecate_positional_args
def make_axis_labels(vmin, vmax, n, fmt): # noqa: PLR0917
"""Create axis labels as a :vtk:`vtkStringArray`.
Parameters
----------
vmin : float
The minimum value for the axis labels.
vmax : float
The maximum value for the axis labels.
n : int
The number of labels to create.
fmt : str
A format string for the labels. If the string starts with '%', the label will be formatted
using the old-style string formatting method.
Otherwise, the label will be formatted using the new-style string formatting method.
Returns
-------
:vtk:`vtkStringArray`
The created labels as a :vtk:`vtkStringArray` object.
"""
labels = _vtk.vtkStringArray()
for v in np.linspace(vmin, vmax, n):
label = (fmt % v if fmt.startswith('%') else fmt.format(v)) if fmt else f'{v}'
labels.InsertNextValue(label)
return labels
class CubeAxesActor(
_NoNewAttrMixin, _NameMixin, _BoundsSizeMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkCubeAxesActor
):
"""Wrap :vtk:`vtkCubeAxesActor`.
This class is created to wrap :vtk:`vtkCubeAxesActor`, which is used to draw axes
and labels for the input data bounds. This wrapping aims to provide a
user-friendly interface to use :vtk:`vtkCubeAxesActor`.
Parameters
----------
camera : pyvista.Camera
Camera to link to the axes actor.
minor_ticks : bool, default: False
If ``True``, also plot minor ticks on all axes.
tick_location : str, optional
Set how the ticks are drawn on the axes grid. Options include:
``'inside', 'outside', 'both'``.
x_title : str, default: "X Axis"
Title of the x-axis.
y_title : str, default: "Y Axis"
Title of the y-axis.
z_title : str, default: "Z Axis"
Title of the z-axis.
x_axis_visibility : bool, default: True
Visibility of the x-axis.
y_axis_visibility : bool, default: True
Visibility of the y-axis.
z_axis_visibility : bool, default: True
Visibility of the z-axis.
x_label_format : str, optional
A format string defining how tick labels are generated from tick
positions for the x-axis. Defaults to the theme format if set,
otherwise ``'%.1f'``.
y_label_format : str, optional
A format string defining how tick labels are generated from tick
positions for the y-axis. Defaults to the theme format if set,
otherwise ``'%.1f'``.
z_label_format : str, optional
A format string defining how tick labels are generated from tick
positions for the z-axis. Defaults to the theme format if set,
otherwise ``'%.1f'``.
x_label_visibility : bool, default: True
The visibility of the x-axis labels.
y_label_visibility : bool, default: True
The visibility of the y-axis labels.
z_label_visibility : bool, default: True
The visibility of the z-axis labels.
n_xlabels : int, default: 5
Number of labels along the x-axis.
n_ylabels : int, default: 5
Number of labels along the y-axis.
n_zlabels : int, default: 5
Number of labels along the z-axis.
See Also
--------
:meth:`~pyvista.Plotter.show_bounds`
:meth:`~pyvista.Plotter.show_grid`
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Create a 3D plotter and add a CubeAxesActor to it.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> cube_axes_actor = pv.CubeAxesActor(pl.camera)
>>> cube_axes_actor.bounds = mesh.bounds
>>> actor, property = pl.add_actor(cube_axes_actor)
>>> pl.show()
"""
@_deprecate_positional_args(allowed=['camera'])
def __init__( # noqa: PLR0917
self,
camera,
minor_ticks: bool = False, # noqa: FBT001, FBT002
tick_location=None,
x_title='X Axis',
y_title='Y Axis',
z_title='Z Axis',
x_axis_visibility: bool = True, # noqa: FBT001, FBT002
y_axis_visibility: bool = True, # noqa: FBT001, FBT002
z_axis_visibility: bool = True, # noqa: FBT001, FBT002
x_label_format=None,
y_label_format=None,
z_label_format=None,
x_label_visibility: bool = True, # noqa: FBT001, FBT002
y_label_visibility: bool = True, # noqa: FBT001, FBT002
z_label_visibility: bool = True, # noqa: FBT001, FBT002
n_xlabels=5,
n_ylabels=5,
n_zlabels=5,
):
"""Initialize CubeAxesActor."""
super().__init__()
self.camera = camera
# empty string used for clearing axis labels
self._empty_str = _vtk.vtkStringArray()
self._empty_str.InsertNextValue('')
# stop labels from being generated several times during init
self.x_axis_visibility = False
self.y_axis_visibility = False
self.z_axis_visibility = False
if not minor_ticks:
self.x_axis_minor_tick_visibility = minor_ticks
self.y_axis_minor_tick_visibility = minor_ticks
self.z_axis_minor_tick_visibility = minor_ticks
if tick_location:
self.tick_location = tick_location
self.x_title = x_title
self.y_title = y_title
self.z_title = z_title
self._x_label_visibility = x_label_visibility
self._y_label_visibility = y_label_visibility
self._z_label_visibility = z_label_visibility
if x_label_format is None:
x_label_format = pyvista.global_theme.font.fmt
if x_label_format is None:
x_label_format = '%.1f'
if y_label_format is None:
y_label_format = pyvista.global_theme.font.fmt
if y_label_format is None:
y_label_format = '%.1f'
if z_label_format is None:
z_label_format = pyvista.global_theme.font.fmt
if z_label_format is None:
z_label_format = '%.1f'
self.x_label_format = x_label_format
self.y_label_format = y_label_format
self.z_label_format = z_label_format
self.n_xlabels = n_xlabels
self.n_ylabels = n_ylabels
self.n_zlabels = n_zlabels
self.x_axis_visibility = x_axis_visibility
self.y_axis_visibility = y_axis_visibility
self.z_axis_visibility = z_axis_visibility
@property
def tick_location(self) -> str: # numpydoc ignore=RT01
"""Return or set how the ticks are drawn on the axes grid.
Options include: ``'inside', 'outside', 'both'``.
"""
tloc = self.GetTickLocation()
if tloc == 0:
return 'inside'
if tloc == 1:
return 'outside'
return 'both'
@tick_location.setter
def tick_location(self, value: str):
if not isinstance(value, str):
msg = f'`tick_location` must be a string, not {type(value)}' # type: ignore[unreachable]
raise TypeError(msg)
value = value.lower()
if value in ('inside'):
self.SetTickLocationToInside()
elif value in ('outside'):
self.SetTickLocationToOutside()
elif value in ('both'):
self.SetTickLocationToBoth()
else:
msg = (
f'Value of tick_location ("{value}") should be either "inside", "outside", '
'or "both".'
)
raise ValueError(msg)
@property
def bounds(self) -> BoundsTuple: # numpydoc ignore=RT01
"""Return or set the bounding box."""
return BoundsTuple(*self.GetBounds())
@bounds.setter
def bounds(self, bounds: VectorLike[float]):
self.SetBounds(bounds) # type: ignore[arg-type]
self._update_labels()
bnds = self.bounds
self.x_axis_range = bnds.x_min, bnds.x_max
self.y_axis_range = bnds.y_min, bnds.y_max
self.z_axis_range = bnds.z_min, bnds.z_max
@property
def center(self) -> tuple[float, float, float]:
"""Return the center.
Returns
-------
tuple[float, float, float]
Center of axes actor.
"""
return self.GetCenter()
@property
def x_axis_range(self) -> tuple[float, float]: # numpydoc ignore=RT01
"""Return or set the x-axis range."""
return self.GetXAxisRange()
@x_axis_range.setter
def x_axis_range(self, value: tuple[float, float]):
self.SetXAxisRange(value)
self._update_x_labels()
@property
def y_axis_range(self) -> tuple[float, float]: # numpydoc ignore=RT01
"""Return or set the y-axis range."""
return self.GetYAxisRange()
@y_axis_range.setter
def y_axis_range(self, value: tuple[float, float]):
self.SetYAxisRange(value)
self._update_y_labels()
@property
def z_axis_range(self) -> tuple[float, float]: # numpydoc ignore=RT01
"""Return or set the z-axis range."""
return self.GetZAxisRange()
@z_axis_range.setter
def z_axis_range(self, value: tuple[float, float]):
self.SetZAxisRange(value)
self._update_z_labels()
@property
def label_offset(self) -> float: # numpydoc ignore=RT01
"""Return or set the distance between labels and the axis."""
return self.GetLabelOffset()
@label_offset.setter
def label_offset(self, offset: float):
self.SetLabelOffset(offset)
@property
def title_offset(self) -> float | tuple[float, float]: # numpydoc ignore=RT01
"""Return or set the distance between title and labels."""
if (9, 3, 0) <= pyvista.vtk_version_info < (9, 5, 0):
offx, offy = (_vtk.reference(0.0), _vtk.reference(0.0))
self.GetTitleOffset(offx, offy) # type: ignore[call-arg]
return offx, offy # type: ignore[return-value]
return self.GetTitleOffset()
@title_offset.setter
def title_offset(self, offset: float | MutableSequence[float]):
vtk_geq_9_3 = pyvista.vtk_version_info >= (9, 3)
if vtk_geq_9_3:
if isinstance(offset, float):
msg = (
f'Setting title_offset with a float is deprecated from vtk >= 9.3. '
f'Accepts now a sequence of (x,y) offsets. '
f'Setting the x offset to {(x := 0.0)}'
)
warnings.warn(msg, UserWarning)
self.SetTitleOffset([x, offset])
else:
self.SetTitleOffset(offset)
return
if isinstance(offset, MutableSequence):
msg = (
f'Setting title_offset with a sequence is only supported from vtk >= 9.3. '
f'Considering only the second value (ie. y-offset) of {(y := offset[1])}'
)
warnings.warn(msg, UserWarning)
self.SetTitleOffset(y) # type: ignore[arg-type]
return
self.SetTitleOffset(offset) # type: ignore[arg-type]
@property
def camera(self) -> pyvista.Camera: # numpydoc ignore=RT01
"""Return or set the camera that performs scaling and translation."""
return self.GetCamera()
@camera.setter
def camera(self, camera: pyvista.Camera):
self.SetCamera(camera)
@property
def x_axis_minor_tick_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set visibility of the x-axis minior tick."""
return bool(self.GetXAxisMinorTickVisibility())
@x_axis_minor_tick_visibility.setter
def x_axis_minor_tick_visibility(self, value: bool):
self.SetXAxisMinorTickVisibility(value)
@property
def y_axis_minor_tick_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set visibility of the y-axis minior tick."""
return bool(self.GetYAxisMinorTickVisibility())
@y_axis_minor_tick_visibility.setter
def y_axis_minor_tick_visibility(self, value: bool):
self.SetYAxisMinorTickVisibility(value)
@property
def z_axis_minor_tick_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set visibility of the z-axis minior tick."""
return bool(self.GetZAxisMinorTickVisibility())
@z_axis_minor_tick_visibility.setter
def z_axis_minor_tick_visibility(self, value: bool):
self.SetZAxisMinorTickVisibility(value)
@property
def x_label_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the x-axis labels."""
return self._x_label_visibility
@x_label_visibility.setter
def x_label_visibility(self, value: bool):
self._x_label_visibility = bool(value)
self._update_x_labels()
@property
def y_label_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the y-axis labels."""
return self._y_label_visibility
@y_label_visibility.setter
def y_label_visibility(self, value: bool):
self._y_label_visibility = bool(value)
self._update_y_labels()
@property
def z_label_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the z-axis labels."""
return self._z_label_visibility
@z_label_visibility.setter
def z_label_visibility(self, value: bool):
self._z_label_visibility = bool(value)
self._update_z_labels()
@property
def x_axis_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the x-axis."""
return bool(self.GetXAxisVisibility())
@x_axis_visibility.setter
def x_axis_visibility(self, value: bool):
self.SetXAxisVisibility(value)
@property
def y_axis_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the y-axis."""
return bool(self.GetYAxisVisibility())
@y_axis_visibility.setter
def y_axis_visibility(self, value: bool):
self.SetYAxisVisibility(value)
@property
def z_axis_visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set the visibility of the y-axis."""
return bool(self.GetZAxisVisibility())
@z_axis_visibility.setter
def z_axis_visibility(self, value: bool):
self.SetZAxisVisibility(value)
@property
def x_label_format(self) -> str: # numpydoc ignore=RT01
"""Return or set the label of the x-axis."""
return self.GetXLabelFormat()
@x_label_format.setter
def x_label_format(self, value: str):
self.SetXLabelFormat(value)
self._update_x_labels()
@property
def y_label_format(self) -> str: # numpydoc ignore=RT01
"""Return or set the label of the y-axis."""
return self.GetYLabelFormat()
@y_label_format.setter
def y_label_format(self, value: str):
self.SetYLabelFormat(value)
self._update_y_labels()
@property
def z_label_format(self) -> str: # numpydoc ignore=RT01
"""Return or set the label of the z-axis."""
return self.GetZLabelFormat()
@z_label_format.setter
def z_label_format(self, value: str):
self.SetZLabelFormat(value)
self._update_z_labels()
@property
def x_title(self) -> str: # numpydoc ignore=RT01
"""Return or set the title of the x-axis."""
return self._x_title
@x_title.setter
def x_title(self, value: str):
self._x_title = value
self._update_x_labels()
@property
def y_title(self) -> str: # numpydoc ignore=RT01
"""Return or set the title of the y-axis."""
return self._y_title
@y_title.setter
def y_title(self, value: str):
self._y_title = value
self._update_y_labels()
@property
def z_title(self) -> str: # numpydoc ignore=RT01
"""Return or set the title of the z-axis."""
return self._z_title
@z_title.setter
def z_title(self, value: str):
self._z_title = value
self._update_z_labels()
@property
def use_2d_mode(self) -> bool: # numpydoc ignore=RT01
"""Use the 2d render mode.
This can be enabled for smoother plotting.
"""
return bool(self.GetUse2DMode())
@use_2d_mode.setter
def use_2d_mode(self, value: bool):
self.SetUse2DMode(value)
@property
def n_xlabels(self): # numpydoc ignore=RT01
"""Number of labels on the x-axis."""
return self._n_xlabels
@n_xlabels.setter
def n_xlabels(self, value: int):
self._n_xlabels = value
self._update_x_labels()
@property
def n_ylabels(self): # numpydoc ignore=RT01
"""Number of labels on the y-axis."""
return self._n_ylabels
@n_ylabels.setter
def n_ylabels(self, value: int):
self._n_ylabels = value
self._update_y_labels()
@property
def n_zlabels(self): # numpydoc ignore=RT01
"""Number of labels on the z-axis."""
return self._n_zlabels
@n_zlabels.setter
def n_zlabels(self, value: int):
self._n_zlabels = value
self._update_z_labels()
def _update_labels(self):
"""Update all labels."""
self._update_x_labels()
self._update_y_labels()
self._update_z_labels()
def _update_x_labels(self):
"""Regenerate x-axis labels."""
if self.x_axis_visibility:
self.SetXTitle(self._x_title)
if self._x_label_visibility:
vmin, vmax = self.x_axis_range
self.SetAxisLabels(
0,
make_axis_labels(
vmin=vmin, vmax=vmax, n=self.n_xlabels, fmt=self.x_label_format
),
)
else:
self.SetAxisLabels(0, self._empty_str)
else:
self.SetXTitle(' ')
self.SetAxisLabels(0, self._empty_str)
def _update_y_labels(self):
"""Regenerate y-axis labels."""
if self.y_axis_visibility:
self.SetYTitle(self._y_title)
if self._y_label_visibility:
vmin, vmax = self.y_axis_range
self.SetAxisLabels(
1,
make_axis_labels(
vmin=vmin, vmax=vmax, n=self.n_ylabels, fmt=self.y_label_format
),
)
else:
self.SetAxisLabels(1, self._empty_str)
else:
self.SetYTitle(' ')
self.SetAxisLabels(1, self._empty_str)
def _update_z_labels(self):
"""Regenerate z-axis labels."""
if self.z_axis_visibility:
self.SetZTitle(self._z_title)
if self._z_label_visibility:
vmin, vmax = self.z_axis_range
self.SetAxisLabels(
2,
make_axis_labels(
vmin=vmin, vmax=vmax, n=self.n_zlabels, fmt=self.z_label_format
),
)
else:
self.SetAxisLabels(2, self._empty_str)
else:
self.SetZTitle(' ')
self.SetAxisLabels(2, self._empty_str)
@property
def x_labels(self) -> list[str]: # numpydoc ignore=RT01
"""Return the x-axis labels."""
labels_vtk = cast('_vtk.vtkStringArray', self.GetAxisLabels(0))
return convert_string_array(labels_vtk).tolist()
@property
def y_labels(self) -> list[str]: # numpydoc ignore=RT01
"""Return the y-axis labels."""
labels_vtk = cast('_vtk.vtkStringArray', self.GetAxisLabels(1))
return convert_string_array(labels_vtk).tolist()
@property
def z_labels(self) -> list[str]: # numpydoc ignore=RT01
"""Return the z-axis labels."""
labels_vtk = cast('_vtk.vtkStringArray', self.GetAxisLabels(2))
return convert_string_array(labels_vtk).tolist()
def update_bounds(self, bounds):
"""Update the bounds of this actor.
Unlike the :attr:`CubeAxesActor.bounds` attribute, updating the bounds
also updates the axis labels.
Parameters
----------
bounds : sequence[float]
Bounds in the form of ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
"""
self.bounds = bounds
@@ -0,0 +1,36 @@
"""Plotting errors."""
from __future__ import annotations
CAMERA_ERROR_MESSAGE = """Invalid camera description
Camera description must be one of the following:
Iterable containing position, focal_point, and view up. For example:
[(2.0, 5.0, 13.0), (0.0, 0.0, 0.0), (-0.7, -0.5, 0.3)]
Iterable containing a view vector. For example:
[-1.0, 2.0, -5.0]
A string containing the plane orthogonal to the view direction. For example:
'xy'
"""
class InvalidCameraError(ValueError): # numpydoc ignore=PR01
"""Exception when passed an invalid camera."""
def __init__(self, message=CAMERA_ERROR_MESSAGE):
"""Call the base class constructor with the custom message."""
super().__init__(message)
class RenderWindowUnavailable(RuntimeError): # numpydoc ignore=PR01 # noqa: N818
"""Exception when the render window is not available."""
def __init__(self, message='Render window is not available.'):
"""Call the base class constructor with the custom message."""
super().__init__(message)
class PyVistaPickingError(RuntimeError):
"""General picking error class."""
@@ -0,0 +1,92 @@
"""Wrap :vtk:`vtkFollower` module."""
from __future__ import annotations
from typing import TYPE_CHECKING
from . import _vtk
from .actor import Actor
if TYPE_CHECKING:
from ._property import Property
from .camera import Camera
from .mapper import _BaseMapper
class Follower(Actor, _vtk.vtkFollower):
"""Wrap :vtk:`vtkFollower`.
A Follower is a subclass of Actor that always faces the camera. It
is useful for screen-aligned labels and billboarding effects.
The Follower maintains the position and scale of the actor but updates
its orientation continuously to face the camera.
Parameters
----------
mapper : pyvista.DataSetMapper, optional
DataSetMapper.
prop : pyvista.Property, optional
Property of the actor.
name : str, optional
The name of this actor used when tracking on a plotter.
Examples
--------
Create a scene with a Follower text that always faces the camera and a transparent cube.
>>> import pyvista as pv
>>> from pyvista import examples
>>> plotter = pv.Plotter()
Create the "Hello" text that will follow the camera.
>>> text_mesh = pv.Text3D('Hello', depth=0.1)
>>> text_mesh = text_mesh.translate(
... [-text_mesh.center[0], -text_mesh.center[1], 0]
... )
Create mapper and follower actor for the text.
>>> text_mapper = pv.DataSetMapper(text_mesh)
>>> follower = pv.Follower(mapper=text_mapper)
>>> follower.prop.color = 'gold'
>>> _ = plotter.add_actor(follower)
Create a transparent cube that doesn't follow the camera.
>>> cube = pv.Cube()
>>> cube_actor = plotter.add_mesh(
... cube, color='MidnightBlue', opacity=0.3, show_edges=False
... )
Set the follower's camera and show the scene.
>>> follower.camera = plotter.camera
>>> plotter.show()
"""
def __init__(
self,
mapper: _BaseMapper | None = None,
prop: Property | None = None,
name: str | None = None,
) -> None:
"""Initialize follower."""
super().__init__(mapper=mapper, prop=prop, name=name)
@property
def camera(self) -> Camera | None: # numpydoc ignore=RT01
"""Return or set the camera of this follower.
The follower will continuously update its orientation to face this camera.
"""
return self.GetCamera() # type: ignore[return-value]
@camera.setter
def camera(self, cam: Camera) -> None:
self.SetCamera(cam)
@@ -0,0 +1,202 @@
"""Convenience helper functions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.helpers import is_pyvista_dataset
if TYPE_CHECKING:
from pyvista.core._typing_core import NumpyArray
def plot_arrows(cent, direction, **kwargs):
"""Plot arrows as vectors.
Parameters
----------
cent : array_like[float]
Accepts a single 3d point or array of 3d points.
direction : array_like[float]
Accepts a single 3d point or array of 3d vectors.
Must contain the same number of items as ``cent``.
**kwargs : dict, optional
See :func:`pyvista.plot`.
Returns
-------
tuple
See the returns of :func:`pyvista.plot`.
See Also
--------
pyvista.plot
Examples
--------
Plot a single random arrow.
>>> import numpy as np
>>> import pyvista as pv
>>> rng = np.random.default_rng(seed=0)
>>> cent = rng.random(3)
>>> direction = rng.random(3)
>>> pv.plot_arrows(cent, direction)
Plot 100 random arrows.
>>> import numpy as np
>>> import pyvista as pv
>>> cent = rng.random((100, 3))
>>> direction = rng.random((100, 3))
>>> pv.plot_arrows(cent, direction)
"""
return pyvista.plot([cent, direction], **kwargs)
@_deprecate_positional_args(allowed=['data_a', 'data_b', 'data_c', 'data_d'], n_allowed=4)
def plot_compare_four( # noqa: PLR0917
data_a,
data_b,
data_c,
data_d,
display_kwargs=None,
plotter_kwargs=None,
show_kwargs=None,
screenshot=None,
camera_position=None,
outline=None,
outline_color='k',
labels=('A', 'B', 'C', 'D'),
link: bool = True, # noqa: FBT001, FBT002
notebook=None,
):
"""Plot a 2 by 2 comparison of data objects.
Parameters
----------
data_a : pyvista.DataSet
The data object to display in the top-left corner.
data_b : pyvista.DataSet
The data object to display in the top-right corner.
data_c : pyvista.DataSet
The data object to display in the bottom-left corner.
data_d : pyvista.DataSet
The data object to display in the bottom-right corner.
display_kwargs : dict, default: None
Additional keyword arguments to pass to the ``add_mesh`` method.
plotter_kwargs : dict, default: None
Additional keyword arguments to pass to the ``Plotter`` constructor.
show_kwargs : dict, default: None
Additional keyword arguments to pass to the ``show`` method.
screenshot : str or bool, default: None
File name or path to save screenshot of the plot, or ``True`` to return
a screenshot array.
camera_position : list, default: None
The camera position to use in the plot.
outline : pyvista.DataSet, default: None
An outline to plot around the data objects.
outline_color : str, default: 'k'
The color of the outline.
labels : tuple of str, default: ('A', 'B', 'C', 'D')
The labels to display for each data object.
link : bool, default: True
If ``True``, link the views of the subplots.
notebook : bool, default: None
If ``True``, display the plot in a Jupyter notebook.
Returns
-------
pyvista.Plotter
The plotter object.
"""
datasets = [[data_a, data_b], [data_c, data_d]]
labels = [labels[0:2], labels[2:4]]
if plotter_kwargs is None:
plotter_kwargs = {}
if display_kwargs is None:
display_kwargs = {}
if show_kwargs is None:
show_kwargs = {}
plotter_kwargs['notebook'] = notebook
pl = pyvista.Plotter(shape=(2, 2), **plotter_kwargs)
for i in range(2):
for j in range(2):
pl.subplot(i, j)
pl.add_mesh(datasets[i][j], **display_kwargs)
pl.add_text(labels[i][j])
if is_pyvista_dataset(outline):
pl.add_mesh(outline, color=outline_color)
if camera_position is not None:
pl.camera_position = camera_position
if link:
pl.link_views()
# when linked, camera must be reset such that the view range
# of all subrender windows matches
pl.reset_camera()
return pl.show(screenshot=screenshot, **show_kwargs)
@_deprecate_positional_args(allowed=['view'])
def view_vectors(view: str, negative: bool = False) -> tuple[NumpyArray[int], NumpyArray[int]]: # noqa: FBT001, FBT002
"""Given a plane to view, return vectors for setting up camera.
Parameters
----------
view : {'xy', 'yx', 'xz', 'zx', 'yz', 'zy'}
Plane to return vectors for.
negative : bool, default: False
Whether to view from opposite direction.
Returns
-------
vec : numpy.ndarray
``[x, y, z]`` vector that points in the viewing direction.
viewup : numpy.ndarray
``[x, y, z]`` vector that points to the viewup direction.
"""
if view == 'xy':
vec = np.array([0, 0, 1])
viewup = np.array([0, 1, 0])
elif view == 'yx':
vec = np.array([0, 0, -1])
viewup = np.array([1, 0, 0])
elif view == 'xz':
vec = np.array([0, -1, 0])
viewup = np.array([0, 0, 1])
elif view == 'zx':
vec = np.array([0, 1, 0])
viewup = np.array([1, 0, 0])
elif view == 'yz':
vec = np.array([1, 0, 0])
viewup = np.array([0, 0, 1])
elif view == 'zy':
vec = np.array([-1, 0, 0])
viewup = np.array([0, 1, 0])
else:
msg = (
f'Unexpected value for direction {view}\n'
" Expected: 'xy', 'yx', 'xz', 'zx', 'yz', 'zy'"
)
raise ValueError(msg)
if negative:
vec *= -1
return vec, viewup
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
"""Module with enum options classes for plotting."""
from __future__ import annotations
from pyvista.core.utilities.misc import AnnotatedIntEnum
class InterpolationType(AnnotatedIntEnum):
"""Lighting interpolation types.
Attributes
----------
FLAT : (int, str)
Flat interpolation type.
GOURAUD : (int, str)
Gouraud interpolation type.
PHONG : (int, str)
Phong interpolation type.
PBR : (int, str)
Physically based rendering interpolation type.
"""
FLAT = (0, 'Flat')
GOURAUD = (1, 'Gouraud')
PHONG = (2, 'PHONG')
PBR = (3, 'Physically based rendering')
@classmethod
def from_str(cls, input_str):
"""Create from string.
Create an instance of InterpolationType from a string.
Parameters
----------
input_str : str
The string representation of the interpolation type. Accepts
aliases such as ``'pbr'`` for ``'Physically based rendering'``.
Returns
-------
InterpolationType
Interpolation type as defined by the input string.
"""
aliases = {
'pbr': 'Physically based rendering',
}
if input_str in aliases:
input_str = aliases[input_str]
return super().from_str(input_str)
class RepresentationType(AnnotatedIntEnum):
"""Types of representations the models can have."""
POINTS = (0, 'Points')
WIREFRAME = (1, 'Wireframe')
SURFACE = (2, 'Surface')
class ElementType(AnnotatedIntEnum):
"""Types of elemental geometries."""
MESH = (0, 'Mesh')
CELL = (1, 'Cell')
FACE = (2, 'Face')
EDGE = (3, 'Edge')
POINT = (4, 'Point')
class PickerType(AnnotatedIntEnum):
"""Types of pickers."""
AREA = (0, 'Area')
CELL = (1, 'Cell')
HARDWARE = (2, 'Hardware')
POINT = (3, 'Point')
PROP = (4, 'Prop')
RENDERED = (5, 'Rendered')
RESLICE = (6, 'Reslice')
SCENE = (7, 'Scene')
VOLUME = (8, 'Volume')
WORLD = (9, 'World')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
"""Deprecated pyvista.plotting.plotting module."""
from __future__ import annotations
import importlib
import inspect
import warnings
from pyvista.core.errors import PyVistaDeprecationWarning
def __getattr__(name):
module = importlib.import_module('pyvista.plotting.plotter')
try:
value = inspect.getattr_static(module, name)
except AttributeError:
module = importlib.import_module('pyvista.plotting')
try:
value = inspect.getattr_static(module, name)
except AttributeError:
msg = (
f'Module `pyvista.plotting.plotting` has been deprecated and we could not '
f'automatically find `{name}`.'
)
raise AttributeError(msg) from None
import_path = f'from pyvista.plotting import {name}'
message = (
f'The `pyvista.plotting.plotting` module has been deprecated. '
f'`{name}` is now imported as: `{import_path}`.'
)
warnings.warn(
message,
PyVistaDeprecationWarning,
)
return value
@@ -0,0 +1,688 @@
"""Prop3D module."""
from __future__ import annotations
from abc import ABC
from abc import abstractmethod
from functools import wraps
from typing import TYPE_CHECKING
from typing import Literal
import numpy as np
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core._typing_core import BoundsTuple
from pyvista.core.utilities.arrays import array_from_vtkmatrix
from pyvista.core.utilities.arrays import vtkmatrix_from_array
from pyvista.core.utilities.misc import _BoundsSizeMixin
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from pyvista.core.utilities.transform import Transform
from pyvista.plotting import _vtk
if TYPE_CHECKING:
from typing_extensions import Self
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import RotationLike
from pyvista.core._typing_core import TransformLike
from pyvista.core._typing_core import VectorLike
class Prop3D(
_NoNewAttrMixin, _NameMixin, _BoundsSizeMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkProp3D
):
"""Prop3D wrapper for :vtk:`vtkProp3D`.
Used to represent an entity in a rendering scene. It provides spatial
properties and methods relating to an entity's position, orientation
and scale. It is used as parent class for :class:`pyvista.Actor`,
:class:`pyvista.AxesActor`, and :class:`pyvista.plotting.volume.Volume`.
``Prop3D`` applies transformations in the following order:
#. Translate entity to its :attr:`~origin`.
#. Scale entity by the values in :attr:`~scale`.
#. Rotate entity using the values in :attr:`~orientation`. Internally, rotations are
applied in the order :func:`~rotate_y`, then :func:`~rotate_x`, then :func:`~rotate_z`.
#. Translate entity away from its origin and to its :attr:`~position`.
#. Transform entity with :attr:`~user_matrix`.
"""
def __init__(self) -> None:
"""Initialize Prop3D."""
super().__init__()
@property
def scale(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set entity scale.
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then change the
scale of the actor.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> actor.scale = (2.0, 2.0, 2.0)
>>> actor.scale
(2.0, 2.0, 2.0)
"""
return self.GetScale()
@scale.setter
def scale(self, value: float | VectorLike[float]) -> None:
self.SetScale(value) # type: ignore[arg-type]
@property
def position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the entity position.
Examples
--------
Change the position of an actor. Note how this does not change the
position of the underlying dataset, just the relative location of the
actor in the :class:`pyvista.Plotter`.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(mesh, color='r')
>>> actor.position = (0, 0, 1) # shifts the red sphere up
>>> pl.show()
"""
return self.GetPosition()
@position.setter
def position(self, value: VectorLike[float]) -> None:
self.SetPosition(value) # type: ignore[call-overload]
def rotate_x(self, angle: float) -> None:
"""Rotate the entity about the x-axis.
Parameters
----------
angle : float
Angle to rotate the entity about the x-axis in degrees.
Examples
--------
Rotate the actor about the x-axis 45 degrees. Note how this does not
change the location of the underlying dataset.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> actor.rotate_x(45)
>>> pl.show_axes()
>>> pl.show()
"""
self.RotateX(angle)
def rotate_y(self, angle: float) -> None:
"""Rotate the entity about the y-axis.
Parameters
----------
angle : float
Angle to rotate the entity about the y-axis in degrees.
Examples
--------
Rotate the actor about the y-axis 45 degrees. Note how this does not
change the location of the underlying dataset.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> actor.rotate_y(45)
>>> pl.show_axes()
>>> pl.show()
"""
self.RotateY(angle)
def rotate_z(self, angle: float) -> None:
"""Rotate the entity about the z-axis.
Parameters
----------
angle : float
Angle to rotate the entity about the z-axis in degrees.
Examples
--------
Rotate the actor about the z-axis 45 degrees. Note how this does not
change the location of the underlying dataset.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> actor.rotate_z(45)
>>> pl.show_axes()
>>> pl.show()
"""
self.RotateZ(angle)
@property
def orientation(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the entity orientation angles.
Orientation angles of the axes which define rotations about the
world's x-y-z axes. The angles are specified in degrees and in
x-y-z order. However, the actual rotations are applied in the
following order: :func:`~rotate_y` first, then :func:`~rotate_x`
and finally :func:`~rotate_z`.
Rotations are applied about the specified :attr:`~origin`.
See Also
--------
rotation_from
Alternative method for setting the :attr:`orientation`.
Examples
--------
Reorient just the actor and plot it. Note how the actor is rotated
about the origin ``(0, 0, 0)`` by default.
>>> import pyvista as pv
>>> mesh = pv.Cube(center=(0, 0, 3))
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> actor.orientation = (45, 0, 0)
>>> _ = pl.add_axes_at_origin()
>>> pl.show()
Repeat the last example, but this time reorient the actor about
its center by specifying its :attr:`~origin`.
>>> import pyvista as pv
>>> mesh = pv.Cube(center=(0, 0, 3))
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> actor.origin = actor.center
>>> actor.orientation = (45, 0, 0)
>>> _ = pl.add_axes_at_origin()
>>> pl.show()
Show that the orientation changes with rotation.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> actor.rotate_x(90)
>>> actor.orientation # doctest:+SKIP
(90, 0, 0)
Set the orientation directly.
>>> actor.orientation = (0, 45, 45)
>>> actor.orientation # doctest:+SKIP
(0, 45, 45)
"""
return self.GetOrientation()
@orientation.setter
def orientation(self, value: VectorLike[float]) -> None:
self.SetOrientation(value) # type: ignore[call-overload]
@property
def origin(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the entity origin.
This is the point about which all rotations take place.
See :attr:`~orientation` for examples.
"""
return self.GetOrigin()
@origin.setter
def origin(self, value: VectorLike[float]) -> None:
self.SetOrigin(value) # type: ignore[arg-type]
@property
def bounds(self) -> BoundsTuple: # numpydoc ignore=RT01
"""Return the bounds of the entity.
Bounds are ``(x_min, x_max, y_min, y_max, z_min, z_max)``
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> mesh = pv.Cube(x_length=0.1, y_length=0.2, z_length=0.3)
>>> actor = pl.add_mesh(mesh)
>>> actor.bounds
BoundsTuple(x_min = -0.05,
x_max = 0.05,
y_min = -0.1,
y_max = 0.1,
z_min = -0.15,
z_max = 0.15)
"""
return BoundsTuple(*self.GetBounds())
@property
def center(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return the center of the entity.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere(center=(0.5, 0.5, 1)))
>>> actor.center # doctest:+SKIP
(0.5, 0.5, 1)
"""
return self.GetCenter()
@property
def user_matrix(self) -> NumpyArray[float]: # numpydoc ignore=RT01
"""Return or set the user matrix.
In addition to the instance variables such as position and orientation, the user
can add an additional transformation to the actor.
This matrix is concatenated with the actor's internal transformation that is
implicitly created when the actor is created. This affects the actor/rendering
only, not the input data itself.
The user matrix is the last transformation applied to the actor before
rendering.
See Also
--------
transform
Apply a transformation to the :attr:`user_matrix`.
Returns
-------
np.ndarray
A 4x4 transformation matrix.
Examples
--------
Apply a 4x4 translation to a wireframe actor. This 4x4 transformation
effectively translates the actor by one unit in the Z direction,
rotates the actor about the z-axis by approximately 45 degrees, and
shrinks the actor by a factor of 0.5.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='b')
>>> actor = pl.add_mesh(
... mesh,
... color='r',
... style='wireframe',
... line_width=5,
... lighting=False,
... )
>>> arr = [
... [0.707, -0.707, 0, 0],
... [0.707, 0.707, 0, 0],
... [0, 0, 1, 1.5],
... [0, 0, 0, 2],
... ]
>>> actor.user_matrix = arr
>>> pl.show_axes()
>>> pl.show()
"""
if self.GetUserMatrix() is None:
self.SetUserMatrix(vtkmatrix_from_array(np.eye(4)))
return array_from_vtkmatrix(self.GetUserMatrix())
@user_matrix.setter
def user_matrix(self, value: TransformLike) -> None:
array = np.eye(4) if value is None else _validation.validate_transform4x4(value)
self.SetUserMatrix(vtkmatrix_from_array(array))
def transform(
self,
trans: TransformLike,
multiply_mode: Literal['pre', 'post'] = 'post',
*,
inplace: bool = False,
):
"""Apply a transformation to this object's :attr:`user_matrix`.
.. note::
This applies a transformation by modifying the :attr:`user_matrix`. This
differs from methods like :meth:`rotate_x`, :meth:`rotate_y`, :meth:`rotate_z`,
and :meth:`rotation_from` which apply a transformation indirectly by modifying
the :attr:`orientation`. See the :class:`Prop3D` class description for more
information about how this class is transformed.
.. versionadded:: 0.45
Parameters
----------
trans : TransformLike
Transformation matrix as a 3x3 or 4x4 array, :vtk:`vtkMatrix3x3` or
:vtk:`vtkMatrix4x4`, :vtk:`vtkTransform`, or a SciPy ``Rotation`` instance.
If the input is 3x3, the array is padded using a 4x4 identity matrix.
multiply_mode : 'pre' | 'post', default: 'post'
Multiplication mode to use.
- ``'pre'``: pre-multiply ``trans`` with the :attr:`user_matrix`, i.e.
``user_matrix @ trans``. The transformation is applied `before` the
current user-matrix.
- ``'post'``: post-multiply ``trans`` with the :attr:`user_matrix`, i.e.
``trans @ user_matrix``. The transformation is applied `after` the
current user-matrix.
inplace : bool, default: False
When ``True``, modifies the prop inplace. Otherwise, a copy is returned.
Returns
-------
Prop3D
Transformed prop.
See Also
--------
pyvista.Transform
Describe linear transformations via a 4x4 matrix.
pyvista.DataObjectFilters.transform
Apply a transformation to a mesh.
"""
# Validate input
_validation.check_contains(
['pre', 'post'], must_contain=multiply_mode, name='multiply_mode'
)
matrix = _validation.validate_transform4x4(trans)
# Update user matrix
new_matrix = (
self.user_matrix @ matrix if multiply_mode == 'pre' else matrix @ self.user_matrix
)
output = self if inplace else self.copy()
output.user_matrix = new_matrix
return output
@abstractmethod
@_deprecate_positional_args
def copy(
self: Self,
deep: bool = True, # noqa: FBT001, FBT002
) -> Self: # numpydoc ignore=RT01
"""Return a copy of this prop."""
raise NotImplementedError # pragma: no cover
@property
def length(self) -> float: # numpydoc ignore=RT01
"""Return the length of the entity.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> actor.length
1.7272069317100354
"""
return self.GetLength()
def rotation_from(self, rotation: RotationLike) -> None:
"""Set the entity's orientation from a rotation.
Set the rotation of this entity from a 3x3 rotation matrix. This includes
NumPy arrays, a :vtk:`vtkMatrix3x3`, and SciPy ``Rotation`` objects.
This method may be used as an alternative for setting the :attr:`orientation`.
.. versionadded:: 0.45
Parameters
----------
rotation : RotationLike
3x3 rotation matrix or a SciPy ``Rotation`` object.
Examples
--------
Create an actor and show its initial orientation.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> actor.orientation
(0.0, -0.0, 0.0)
Set the orientation using a 3x3 matrix.
>>> actor.rotation_from([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
>>> actor.orientation
(0.0, -180.0, -89.99999999999999)
"""
self.orientation = _rotation_matrix_as_orientation(rotation) # type: ignore[arg-type]
def _rotation_matrix_as_orientation(
array: NumpyArray[float] | _vtk.vtkMatrix3x3,
) -> tuple[float, float, float]:
"""Convert a 3x3 rotation matrix to x-y-z orientation angles.
The orientation angles define rotations about the world's x-y-z axes. The angles
are specified in degrees and in x-y-z order. However, the rotations should
be applied in the order: first rotate about the y-axis, then x-axis, then z-axis.
The rotation angles and rotation matrix can be used interchangeably for
transformations.
Parameters
----------
array : NumpyArray[float] | :vtk:`vtkMatrix3x3`
3x3 rotation matrix as a NumPy array or a :vtk:`vtkMatrix3x3`.
Returns
-------
tuple
Tuple with x-y-z axis rotation angles in degrees.
"""
return Transform().rotate(array).GetOrientation()
def _orientation_as_rotation_matrix(orientation: VectorLike[float]) -> NumpyArray[float]:
"""Convert x-y-z orientation angles to a 3x3 matrix.
The orientation angles define rotations about the world's x-y-z axes. The angles
are specified in degrees and in x-y-z order. However, the rotations should
be applied in the order: first rotate about the y-axis, then x-axis, then z-axis.
The rotation angles and rotation matrix can be used interchangeably for
transformations.
Parameters
----------
orientation : VectorLike[float]
The x-y-z axis orientation angles in degrees.
Returns
-------
numpy.ndarray
3x3 rotation matrix.
"""
valid_orientation = _validation.validate_array3(orientation, name='orientation')
prop = _vtk.vtkActor()
prop.SetOrientation(valid_orientation)
matrix = _vtk.vtkMatrix4x4()
prop.GetMatrix(matrix)
return array_from_vtkmatrix(matrix)[:3, :3]
class _Prop3DMixin(_BoundsSizeMixin, ABC):
"""Add 3D transformations to props which do not inherit from :class:`pyvista.Prop3D`.
Derived classes need to implement the :meth:`_post_set_update` method to define
their behavior, e.g. manually apply a transformation.
"""
def __init__(self) -> None:
from pyvista import Actor # Avoid circular import # noqa: PLC0415
self._prop3d = Actor()
@property
@wraps(Prop3D.scale.fget) # type: ignore[attr-defined]
def scale(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.scale."""
return self._prop3d.scale
@scale.setter
@wraps(Prop3D.scale.fset) # type: ignore[attr-defined]
def scale(self, scale: VectorLike[float]) -> None:
self._prop3d.scale = scale
self._post_set_update()
@property
@wraps(Prop3D.position.fget) # type: ignore[attr-defined]
def position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.position."""
return self._prop3d.position
@position.setter
@wraps(Prop3D.position.fset) # type: ignore[attr-defined]
def position(self, position: VectorLike[float]) -> None:
self._prop3d.position = position
self._post_set_update()
@property
@wraps(Prop3D.orientation.fget) # type: ignore[attr-defined]
def orientation(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.orientation."""
return self._prop3d.orientation
@orientation.setter
@wraps(Prop3D.orientation.fset) # type: ignore[attr-defined]
def orientation(self, orientation: VectorLike[float]) -> None:
self._prop3d.orientation = orientation
self._post_set_update()
@property
@wraps(Prop3D.origin.fget) # type: ignore[attr-defined]
def origin(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.origin."""
return self._prop3d.origin
@origin.setter
@wraps(Prop3D.origin.fset) # type: ignore[attr-defined]
def origin(self, origin: VectorLike[float]) -> None:
self._prop3d.origin = origin
self._post_set_update()
@property
@wraps(Prop3D.user_matrix.fget) # type: ignore[attr-defined]
def user_matrix(self) -> NumpyArray[float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.user_matrix."""
return self._prop3d.user_matrix
@user_matrix.setter
@wraps(Prop3D.user_matrix.fset) # type: ignore[attr-defined]
def user_matrix(self, matrix: TransformLike) -> None:
self._prop3d.user_matrix = matrix
self._post_set_update()
@property
def _transformation_matrix(self):
"""Transformation matrix applied to the actor.
The transformation is computed from the attributes :attr:`position`
:attr:`origin`, :attr:`scale`, :attr:`orientation`, and :attr:`user_matrix`.
It is the actual transformation applied to the actor under-the-hood by vtk.
"""
return array_from_vtkmatrix(self._prop3d.GetMatrix())
@abstractmethod
def _post_set_update(self):
"""Update object after setting Prop3D attributes."""
@abstractmethod
def _get_bounds(self) -> BoundsTuple:
"""Return the object's 3D bounds."""
@property
@wraps(Prop3D.bounds.fget) # type: ignore[attr-defined]
def bounds(self) -> BoundsTuple: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.bounds`."""
return BoundsTuple(*self._get_bounds())
@property
@wraps(Prop3D.center.fget) # type: ignore[attr-defined]
def center(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.center."""
bnds = self.bounds
return (
(bnds.x_min + bnds.x_max) / 2,
(bnds.y_min + bnds.y_max) / 2,
(bnds.z_min + bnds.z_max) / 2,
)
@property
@wraps(Prop3D.length.fget) # type: ignore[attr-defined]
def length(self) -> float: # numpydoc ignore=RT01
"""Wrap :class:`pyvista.Prop3D.length."""
bnds = self.bounds
return np.linalg.norm(
(bnds.x_max - bnds.x_min, bnds.y_max - bnds.y_min, bnds.z_max - bnds.z_min)
).tolist()
@@ -0,0 +1,118 @@
"""Wrapper for :vtk:`vtkPropCollection`."""
from __future__ import annotations
from collections.abc import Iterable
from collections.abc import MutableSequence
from typing import TYPE_CHECKING
import numpy as np
from pyvista import _validation
from pyvista.plotting import _vtk
if TYPE_CHECKING:
from typing import Any
class _PropCollection(MutableSequence[_vtk.vtkProp]):
"""Sequence wrapper for a :vtk:`vtkPropCollection` with a dict-like interface.
.. versionadded:: 0.45
"""
def __init__(self, prop_collection: _vtk.vtkPropCollection):
super().__init__()
self._prop_collection = prop_collection
def __getitem__(self, key):
if isinstance(key, (int, np.integer)):
# lookup from index number
key = self._validate_index(key)
return self._prop_collection.GetItemAsObject(int(key))
elif isinstance(key, str):
# lookup from actor name
names = self.keys()
try:
index = names.index(key)
return self._prop_collection.GetItemAsObject(index)
except ValueError:
msg = f"No item found with name '{key}'."
raise KeyError(msg)
msg = f'Key must be an index or a string, got {type(key).__name__}.'
raise TypeError(msg)
def __len__(self):
return self._prop_collection.GetNumberOfItems()
def __delitem__(self, key):
if isinstance(key, (int, np.integer)):
# remove by index
key = self._validate_index(key)
self._prop_collection.RemoveItem(key)
elif isinstance(key, str):
# remove by name
names = self.keys()
try:
index = names.index(key)
except ValueError:
msg = f"No item found with name '{key}'."
raise KeyError(msg)
del self[index]
else:
msg = f'Key must be an index or a string, got {type(key).__name__}.'
raise TypeError(msg)
def __setitem__(self, key, value):
_validation.check_instance(value, _vtk.vtkProp)
if isinstance(key, (int, np.integer)):
# set by index
key = self._validate_index(key)
self._prop_collection.ReplaceItem(key, value)
elif isinstance(key, str):
if hasattr(value, 'name') and value.name != key:
msg = f"Name of the new actor '{value.name}' must match the key name '{key}'."
raise ValueError(msg)
# set by name
index = self.keys().index(key)
self[index] = value
else:
msg = f'Key must be an index or a string, got {type(key).__name__}.'
raise TypeError(msg)
def insert(self, index, value) -> None:
_validation.check_instance(value, _vtk.vtkProp)
if len(self) == 0:
self.append(value)
return
if index < 0:
index = len(self) + index + 1
index = min(index, len(self))
self._prop_collection.InsertItem(index - 1, value)
def append(self, value: _vtk.vtkProp):
_validation.check_instance(value, _vtk.vtkProp)
self._prop_collection.AddItem(value)
def keys(self) -> list[str]:
return [
prop.name
if hasattr(prop, 'name')
else f'{type(prop).__name__}({prop.GetAddressAsString("")})'
for prop in self
]
def items(self) -> Iterable[tuple[str, _vtk.vtkProp]]:
yield from zip(self.keys(), self)
def __del__(self):
self._prop_collection = None # type: ignore[assignment]
def _validate_index(self, index: int | np.integer[Any]) -> int:
if index < 0:
index = len(self) + index
if index < 0 or index >= len(self):
msg = 'Index out of range.'
raise IndexError(msg)
return int(index)
@@ -0,0 +1,355 @@
"""Render passes module for PyVista."""
from __future__ import annotations
import weakref
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
# The order of both the pre and post-passes matters.
PRE_PASS = [
'vtkEDLShading',
]
POST_PASS = [
'vtkDepthOfFieldPass',
'vtkGaussianBlurPass',
'vtkOpenGLFXAAPass',
'vtkSSAOPass',
'vtkSSAAPass', # should be last
]
class RenderPasses(_NoNewAttrMixin):
"""Class to support multiple render passes for a renderer.
Notes
-----
Passes are organized here as "primary" (:vtk:`vtkOpenGLRenderPass`) that act
within the renderer and "post-processing" (:vtk:`vtkImageProcessingPass`) passes,
which act on the image generated from the renderer.
The primary passes are added as part of a :vtk:`vtkRenderPassCollection` or
are "stacked", while the post-processing passes are added as a final pass
to the rendered image.
Parameters
----------
renderer : :vtk:`vtkRenderer`
Renderer to initialize render passes for.
"""
def __init__(self, renderer):
"""Initialize render passes."""
self._renderer_ref = weakref.ref(renderer)
self._passes = {}
self._fxaa_pass = None
self._shadow_map_pass = None
self._edl_pass = None
self._dof_pass = None
self._ssaa_pass = None
self._ssao_pass = None
self._blur_passes = []
self.__pass_collection = None
self.__seq_pass = None
self.__camera_pass = None
@property
def _pass_collection(self):
"""Initialize (when necessary) the pass collection and return it.
This lets us lazily generate the pass collection only when we need it
rather than at initialization of the class.
"""
if self.__pass_collection is None:
self._init_passes()
return self.__pass_collection
@property
def _seq_pass(self):
"""Initialize (when necessary) the sequence collection and return it.
This lets us lazily generate the sequence collection only when we need it
rather than at initialization of the class.
"""
if self.__seq_pass is None:
self._init_passes()
return self.__seq_pass
@property
def _camera_pass(self):
"""Initialize (when necessary) the camera pass and return it.
This lets us lazily generate the camera pass only when we need it
rather than at initialization of the class.
"""
if self.__camera_pass is None:
self._init_passes()
return self.__camera_pass
def _init_passes(self):
"""Initialize the renderer's standard passes."""
# simulate the standard VTK rendering passes and put them in a sequence
self.__pass_collection = _vtk.vtkRenderPassCollection()
self.__pass_collection.AddItem(_vtk.vtkRenderStepsPass())
self.__seq_pass = _vtk.vtkSequencePass()
self.__seq_pass.SetPasses(self._pass_collection)
# Make the sequence the delegate of a camera pass.
self.__camera_pass = _vtk.vtkCameraPass()
self.__camera_pass.SetDelegatePass(self._seq_pass)
@property
def _renderer(self):
"""Return the renderer."""
if self._renderer_ref is not None:
return self._renderer_ref()
return None # type: ignore[unreachable]
def deep_clean(self):
"""Delete all render passes."""
if self._renderer is not None:
self._renderer.SetPass(None)
self._renderer_ref = None # type: ignore[assignment]
if self.__seq_pass is not None:
self.__seq_pass.SetPasses(None)
self.__seq_pass = None
self.__pass_collection = None
self.__camera_pass = None
self._passes = {}
self._shadow_map_pass = None
self._edl_pass = None
self._dof_pass = None
self._ssaa_pass = None
self._ssao_pass = None
self._blur_passes = []
def enable_edl_pass(self):
"""Enable the EDL pass.
Returns
-------
:vtk:`vtkEDLShading`
The enabled EDL pass.
"""
if self._edl_pass is not None:
return None
self._edl_pass = _vtk.vtkEDLShading()
self._add_pass(self._edl_pass)
return self._edl_pass
def disable_edl_pass(self):
"""Disable the EDL pass."""
if self._edl_pass is None:
return
self._remove_pass(self._edl_pass)
self._edl_pass = None
def add_blur_pass(self):
"""Add a :vtk:`vtkGaussianBlurPass` pass.
This is a :vtk:`vtkImageProcessingPass` and delegates to the last pass.
Returns
-------
:vtk:`vtkGaussianBlurPass`
The added Gaussian blur pass.
"""
blur_pass = _vtk.vtkGaussianBlurPass()
self._add_pass(blur_pass)
self._blur_passes.append(blur_pass)
return blur_pass
def remove_blur_pass(self):
"""Remove a single :vtk:`vtkGaussianBlurPass` pass."""
if self._blur_passes:
# order of the blur passes does not matter
self._remove_pass(self._blur_passes.pop())
def enable_shadow_pass(self):
"""Enable shadow pass.
Returns
-------
:vtk:`vtkShadowMapPass`
The enabled shadow pass.
"""
# shadow pass can be directly added to the base pass collection
if self._shadow_map_pass is not None:
return None
self._shadow_map_pass = _vtk.vtkShadowMapPass()
self._pass_collection.AddItem(self._shadow_map_pass.GetShadowMapBakerPass())
self._pass_collection.AddItem(self._shadow_map_pass)
self._update_passes()
return self._shadow_map_pass
def disable_shadow_pass(self):
"""Disable shadow pass."""
if self._shadow_map_pass is None:
return
self._pass_collection.RemoveItem(self._shadow_map_pass.GetShadowMapBakerPass())
self._pass_collection.RemoveItem(self._shadow_map_pass)
self._update_passes()
@_deprecate_positional_args
def enable_depth_of_field_pass(self, automatic_focal_distance: bool = True): # noqa: FBT001, FBT002
"""Enable the depth of field pass.
Parameters
----------
automatic_focal_distance : bool, default: True
If ``True``, the depth of field effect will automatically compute
the focal distance. If ``False``, the user must specify the distance.
Returns
-------
:vtk:`vtkDepthOfFieldPass`
The enabled depth of field pass.
"""
if self._dof_pass is not None:
return None
if self._ssao_pass is not None:
msg = 'Depth of field pass is incompatible with the SSAO pass.'
raise RuntimeError(msg)
self._dof_pass = _vtk.vtkDepthOfFieldPass()
self._dof_pass.SetAutomaticFocalDistance(automatic_focal_distance)
self._add_pass(self._dof_pass)
return self._dof_pass
def disable_depth_of_field_pass(self):
"""Disable the depth of field pass."""
if self._dof_pass is None:
return
self._remove_pass(self._dof_pass)
self._dof_pass = None
@_deprecate_positional_args
def enable_ssao_pass( # noqa: PLR0917
self, radius, bias, kernel_size, blur
):
"""Enable the screen space ambient occlusion pass.
Parameters
----------
radius : float
Radius of occlusion generation.
bias : float
Bias to adjust the occlusion generation.
kernel_size : int
Size of the kernel for occlusion generation.
blur : bool
If ``True``, the pass uses a blur stage.
Returns
-------
:vtk:`vtkSSAOPass`
The enabled screen space ambient occlusion pass.
"""
if self._dof_pass is not None:
msg = 'SSAO pass is incompatible with the depth of field pass.'
raise RuntimeError(msg)
if self._ssao_pass is not None:
return None
self._ssao_pass = _vtk.vtkSSAOPass()
self._ssao_pass.SetRadius(radius)
self._ssao_pass.SetBias(bias)
self._ssao_pass.SetKernelSize(kernel_size)
self._ssao_pass.SetBlur(blur)
self._add_pass(self._ssao_pass)
return self._ssao_pass
def disable_ssao_pass(self):
"""Disable the screen space ambient occlusion pass."""
if self._ssao_pass is None:
return
self._remove_pass(self._ssao_pass)
self._ssao_pass = None
def enable_ssaa_pass(self):
"""Enable super-sample anti-aliasing pass.
Returns
-------
:vtk:`vtkSSAAPass`
The enabled super-sample anti-aliasing pass.
"""
if self._ssaa_pass is not None:
return None
self._ssaa_pass = _vtk.vtkSSAAPass()
self._add_pass(self._ssaa_pass)
return self._ssaa_pass
def disable_ssaa_pass(self):
"""Disable super-sample anti-aliasing pass."""
if self._ssaa_pass is None:
return
self._remove_pass(self._ssaa_pass)
self._ssaa_pass = None
def _update_passes(self):
"""Reassemble pass delegation."""
if hasattr(self._renderer, '_closed') and self._renderer._closed:
msg = 'The renderer has been closed.'
raise RuntimeError(msg)
current_pass = self._camera_pass
for class_name in PRE_PASS + POST_PASS:
if class_name in self._passes:
for render_pass in self._passes[class_name]:
render_pass.SetDelegatePass(current_pass)
current_pass = render_pass
# reset to the default rendering if no special passes have been added
if current_pass is self._camera_pass and self._shadow_map_pass is None:
self._renderer.SetPass(None)
else:
self._renderer.SetPass(current_pass)
def _add_pass(self, render_pass):
"""Add a render pass."""
class_name = render_pass.GetClassName()
if class_name in PRE_PASS and render_pass in self._passes:
return
if class_name not in self._passes:
self._passes[class_name] = [render_pass]
else:
self._passes[class_name].append(render_pass)
self._update_passes()
def _remove_pass(self, render_pass):
"""Remove a pass.
Remove a pass and reassemble the pass ordering.
"""
class_name = render_pass.GetClassName()
if class_name not in self._passes: # pragma: no cover
return
else:
self._passes[class_name].remove(render_pass)
if not self._passes[class_name]:
self._passes.pop(class_name)
self._update_passes()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,735 @@
"""Organize Renderers for ``pyvista.Plotter``."""
from __future__ import annotations
from collections.abc import Sequence
from itertools import product
from weakref import proxy
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from .background_renderer import BackgroundRenderer
from .renderer import Renderer
class Renderers(_NoNewAttrMixin):
"""Organize Renderers for ``pyvista.Plotter``.
Parameters
----------
plotter : str
The PyVista plotter.
shape : tuple[int], optional
The initial shape of the PyVista plotter, (rows, columns).
splitting_position : float, optional
The position to place the splitting line between plots.
row_weights : sequence, optional
The weights of the rows when the plot window is resized.
col_weights : sequence, optional
The weights of the columns when the plot window is resized.
groups : list, optional
A list of sequences that defines the grouping of the sub-datasets.
border : bool, optional
Whether or not a border should be added around each subplot.
border_color : str, optional
The color of the border around each subplot.
border_width : float, optional
The width of the border around each subplot.
"""
@_deprecate_positional_args(allowed=['plotter'])
def __init__( # noqa: PLR0917
self,
plotter,
shape=(1, 1),
splitting_position=None,
row_weights=None,
col_weights=None,
groups=None,
border=None,
border_color='k',
border_width=2.0,
):
"""Initialize renderers."""
self._active_index = 0 # index of the active renderer
self._plotter = proxy(plotter)
self._renderers = []
self._shadow_renderer = None
# by default add border for multiple plots
if border is None:
border = shape != (1, 1)
self.groups = np.empty((0, 4), dtype=int)
if isinstance(shape, str):
if '|' in shape:
n = int(shape.split('|')[0])
m = int(shape.split('|')[1])
rangen = reversed(range(n))
rangem = reversed(range(m))
else:
m = int(shape.split('/')[0])
n = int(shape.split('/')[1])
rangen = range(n) # type: ignore[assignment]
rangem = range(m) # type: ignore[assignment]
if splitting_position is None:
splitting_position = pyvista.global_theme.multi_rendering_splitting_position
if splitting_position is None:
xsplit = m / (n + m) if n >= m else 1 - n / (n + m)
else:
xsplit = splitting_position
for i in rangen:
arenderer = Renderer(
self._plotter,
border=border,
border_color=border_color,
border_width=border_width,
)
if '|' in shape:
arenderer.viewport = (0, i / n, xsplit, (i + 1) / n)
else:
arenderer.viewport = (i / n, 0, (i + 1) / n, xsplit)
self._renderers.append(arenderer)
for i in rangem:
arenderer = Renderer(
self._plotter,
border=border,
border_color=border_color,
border_width=border_width,
)
if '|' in shape:
arenderer.viewport = (xsplit, i / m, 1, (i + 1) / m)
else:
arenderer.viewport = (i / m, xsplit, (i + 1) / m, 1)
self._renderers.append(arenderer)
self._shape = (n + m,)
self._render_idxs = np.arange(n + m)
else:
if not isinstance(shape, (np.ndarray, Sequence)):
msg = '"shape" should be a list, tuple or string descriptor'
raise TypeError(msg)
if len(shape) != 2:
msg = '"shape" must have length 2.'
raise ValueError(msg)
shape = np.asarray(shape)
if not np.issubdtype(shape.dtype, np.integer) or (shape <= 0).any():
msg = '"shape" must contain only positive integers.'
raise ValueError(msg)
# always assign shape as a tuple of native ints
self._shape = tuple(size.item() for size in shape)
self._render_idxs = np.empty(self._shape, dtype=int)
# Check if row and col weights correspond to given shape,
# or initialize them to defaults (equally weighted).
# and convert to normalized offsets
if row_weights is None:
row_weights = np.ones(shape[0])
if col_weights is None:
col_weights = np.ones(shape[1])
# also make flattening and abs explicit
row_weights = np.abs(np.asanyarray(row_weights).ravel())
col_weights = np.abs(np.asanyarray(col_weights).ravel())
if row_weights.size != shape[0]:
msg = (
f'"row_weights" must have {shape[0]} items '
f'for {shape[0]} rows of subplots, not '
f'{row_weights.size}.'
)
raise ValueError(msg)
if col_weights.size != shape[1]:
msg = (
f'"col_weights" must have {shape[1]} items '
f'for {shape[1]} columns of subplots, not '
f'{col_weights.size}.'
)
raise ValueError(msg)
row_off = np.cumsum(row_weights) / np.sum(row_weights)
row_off = 1 - np.concatenate(([0], row_off))
col_off = np.cumsum(col_weights) / np.sum(col_weights)
col_off = np.concatenate(([0], col_off))
# Check and convert groups to internal format (Nx4 matrix
# where every row contains the row and col index of the
# top left cell)
if groups is not None:
if not isinstance(groups, Sequence):
msg = f'"groups" should be a list or tuple, not {type(groups).__name__}.'
raise TypeError(msg)
for group in groups:
if not isinstance(group, Sequence):
msg = (
'Each group entry should be a list or '
f'tuple, not {type(group).__name__}.'
)
raise TypeError(msg)
if len(group) != 2:
msg = 'Each group entry must have length 2.'
raise ValueError(msg)
rows = group[0]
if isinstance(rows, slice):
rows = np.arange(self.shape[0], dtype=int)[rows]
cols = group[1]
if isinstance(cols, slice):
cols = np.arange(self.shape[1], dtype=int)[cols] # type: ignore[misc]
# Get the normalized group, i.e. extract top left corner
# and bottom right corner from the given rows and cols
norm_group = [np.min(rows), np.min(cols), np.max(rows), np.max(cols)]
# Check for overlap with already defined groups:
for i, j in product(
range(norm_group[0], norm_group[2] + 1),
range(norm_group[1], norm_group[3] + 1),
):
if self.loc_to_group((i, j)) is not None:
msg = f'Groups cannot overlap. Overlap found at position {(i, j)}.'
raise ValueError(msg)
self.groups = np.concatenate(
(self.groups, np.array([norm_group], dtype=int)),
axis=0,
)
# Create subplot renderers
for row, col in product(range(shape[0]), range(shape[1])):
group = self.loc_to_group((row, col))
nb_rows = None
nb_cols = None
if group is not None:
if row == self.groups[group, 0] and col == self.groups[group, 1]:
# Only add renderer for first location of the group
nb_rows = 1 + self.groups[group, 2] - self.groups[group, 0]
nb_cols = 1 + self.groups[group, 3] - self.groups[group, 1]
else:
nb_rows = 1
nb_cols = 1
if nb_rows is not None:
renderer = Renderer(
self._plotter,
border=border,
border_color=border_color,
border_width=border_width,
)
x0 = col_off[col]
y0 = row_off[row + nb_rows]
x1 = col_off[col + nb_cols] # type: ignore[operator]
y1 = row_off[row]
renderer.viewport = (x0, y0, x1, y1)
self._render_idxs[row, col] = len(self)
self._renderers.append(renderer)
else:
self._render_idxs[row, col] = self._render_idxs[
self.groups[group, 0],
self.groups[group, 1],
]
# each render will also have an associated background renderer
self._background_renderers: list[None | BackgroundRenderer] = [
None for _ in range(len(self))
]
# create a shadow renderer that lives on top of all others
self._shadow_renderer = Renderer(
self._plotter, border=border, border_color=border_color, border_width=border_width
)
self._shadow_renderer.viewport = (0, 0, 1, 1)
self._shadow_renderer.SetDraw(False)
def loc_to_group(self, loc):
"""Return index of the render window given a location index.
Parameters
----------
loc : int | sequence[int]
Index of the renderer to add the actor to. For example, ``loc=2``
or ``loc=(1, 1)``.
Returns
-------
int
Index of the render window.
"""
group_idxs = np.arange(self.groups.shape[0])
index = (
(loc[0] >= self.groups[:, 0])
& (loc[0] <= self.groups[:, 2])
& (loc[1] >= self.groups[:, 1])
& (loc[1] <= self.groups[:, 3])
)
group = group_idxs[index]
return None if group.size == 0 else group[0]
def loc_to_index(self, loc):
"""Return index of the render window given a location index.
Parameters
----------
loc : int | sequence[int]
Index of the renderer to add the actor to. For example, ``loc=2``
or ``loc=(1, 1)``.
Returns
-------
int
Index of the render window.
"""
if isinstance(loc, (int, np.integer)):
return loc
elif isinstance(loc, (np.ndarray, Sequence)):
if len(loc) != 2:
msg = '"loc" must contain two items'
raise ValueError(msg)
index_row = loc[0]
index_column = loc[1]
if index_row < 0 or index_row >= self.shape[0]:
msg = f'Row index is out of range ({self.shape[0]})'
raise IndexError(msg)
if index_column < 0 or index_column >= self.shape[1]: # type: ignore[misc]
msg = f'Column index is out of range ({self.shape[1]})' # type: ignore[misc]
raise IndexError(msg)
return self._render_idxs[index_row, index_column]
else:
msg = '"loc" must be an integer or a sequence.'
raise TypeError(msg)
def __getitem__(self, index):
"""Return a renderer based on an index."""
return self._renderers[index]
def __len__(self):
"""Return number of renderers."""
return len(self._renderers)
def __iter__(self):
"""Return a iterable of renderers."""
yield from self._renderers
@property
def active_index(self): # numpydoc ignore=RT01
"""Return the active index.
Returns
-------
int
Active index.
"""
return self._active_index
def index_to_loc(self, index):
"""Convert a 1D index location to the 2D location on the plotting grid.
Parameters
----------
index : int
A scalar integer that refers to the 1D location index.
Returns
-------
numpy.ndarray or numpy.int64
2D location on the plotting grid.
"""
if not isinstance(index, (int, np.integer)):
msg = '"index" must be a scalar integer.'
raise TypeError(msg)
if len(self.shape) == 1:
return np.intp(index)
args = np.argwhere(self._render_idxs == index)
if len(args) < 1:
msg = f'Index ({index}) is out of range.'
raise IndexError(msg)
return args[0]
@property
def active_renderer(self): # numpydoc ignore=RT01
"""Return the active renderer.
Returns
-------
Renderer
Active renderer.
"""
return self._renderers[self._active_index]
@property
def shape(self) -> tuple[int] | tuple[int, int]:
"""Return the shape of the renderers.
Returns
-------
tuple[int] | tuple[int, int]
Shape of the renderers.
"""
return self._shape
def set_active_renderer(self, index_row, index_column=None):
"""Set the index of the active renderer.
Parameters
----------
index_row : int
Index of the subplot to activate along the rows.
index_column : int, optional
Index of the subplot to activate along the columns.
"""
if len(self.shape) == 1:
self._active_index = index_row
return
if index_row < 0 or index_row >= self.shape[0]:
msg = f'Row index is out of range ({self.shape[0]})'
raise IndexError(msg)
if index_column < 0 or index_column >= self.shape[1]:
msg = f'Column index is out of range ({self.shape[1]})'
raise IndexError(msg)
self._active_index = self.loc_to_index((index_row, index_column))
@_deprecate_positional_args(allowed=['interactive'])
def set_chart_interaction(self, interactive, toggle: bool = False): # noqa: FBT001, FBT002
"""Set or toggle interaction with charts for the active renderer.
Interaction with other charts in other renderers is disabled.
Interaction with other charts in the active renderer is only disabled
when ``toggle`` is ``False``.
Parameters
----------
interactive : bool | Chart | int | sequence[Chart] | sequence[int]
Following parameter values are accepted:
* A boolean to enable (``True``) or disable (``False``) interaction
with all charts in the active renderer.
* The chart or its index to enable interaction with. Interaction
with multiple charts can be enabled by passing a list of charts
or indices.
toggle : bool, default: False
Instead of enabling interaction with the provided chart(s), interaction
with the provided chart(s) is toggled. Only applicable when ``interactive``
is not a boolean.
Returns
-------
list[Chart]
The list of all interactive charts for the active renderer.
"""
interactive_scene, interactive_charts = None, []
if self.active_renderer.has_charts:
interactive_scene = self.active_renderer._charts._scene
interactive_charts = self.active_renderer.set_chart_interaction(
interactive, toggle=toggle
)
# Disable chart interaction for other renderers
for renderer in self:
if renderer is not self.active_renderer:
renderer.set_chart_interaction(False)
# Setup the context interactor style based on the resulting amount of interactive charts.
self._plotter.iren._set_context_style(interactive_scene if interactive_charts else None)
return interactive_charts
def on_plotter_render(self):
"""Notify all renderers of explicit plotter render call."""
for renderer in self:
renderer.on_plotter_render()
def deep_clean(self):
"""Clean all renderers."""
# Do not remove the renderers on the clean
for renderer in self:
renderer.deep_clean()
if self._shadow_renderer is not None:
self._shadow_renderer.deep_clean()
if hasattr(self, '_background_renderers'):
for renderer in self._background_renderers:
if renderer is not None:
renderer.deep_clean()
def add_background_renderer(self, image_path, scale, as_global):
"""Add a background image to the renderers.
Parameters
----------
image_path : str
Path to an image file.
scale : float
Scale the image larger or smaller relative to the size of
the window. For example, a scale size of 2 will make the
largest dimension of the image twice as large as the
largest dimension of the render window. Defaults to 1.
as_global : bool
When multiple render windows are present, setting
``as_global=False`` will cause the background to only
appear in one window.
Returns
-------
pyvista.BackgroundRenderer
Newly created background renderer.
"""
# verify no render exists
if as_global:
for renderer in self:
renderer.layer = 2
view_port = None
else:
self.active_renderer.layer = 2
view_port = self.active_renderer.GetViewport()
renderer = BackgroundRenderer(self._plotter, image_path, scale=scale, view_port=view_port)
renderer.layer = 1
self._background_renderers[self.active_index] = renderer
return renderer
@property
def has_active_background_renderer(self) -> bool: # numpydoc ignore=RT01
"""Return ``True`` when Renderer has an active background renderer.
Returns
-------
bool
Whether or not the active renderer has a background renderer.
"""
return self._background_renderers[self.active_index] is not None
def clear_background_renderers(self):
"""Clear all background renderers."""
for renderer in self._background_renderers:
if renderer is not None:
renderer.clear()
def clear_actors(self):
"""Clear actors from all renderers."""
for renderer in self:
renderer.clear_actors()
def clear(self):
"""Clear all renders."""
for renderer in self:
renderer.clear()
self._shadow_renderer.clear() # type: ignore[union-attr]
self.clear_background_renderers()
def close(self):
"""Close all renderers."""
for renderer in self:
renderer.close()
self._shadow_renderer.close() # type: ignore[union-attr]
for renderer in self._background_renderers:
if renderer is not None:
renderer.close()
def remove_all_lights(self):
"""Remove all lights from all renderers."""
for renderer in self:
renderer.remove_all_lights()
@property
def shadow_renderer(self): # numpydoc ignore=RT01
"""Shadow renderer.
Returns
-------
pyvista.plotting.renderer.Renderer
Shadow renderer.
"""
return self._shadow_renderer
@_deprecate_positional_args(allowed=['color'])
def set_background( # noqa: PLR0917
self,
color,
top=None,
right=None,
side=None,
corner=None,
all_renderers: bool = True, # noqa: FBT001, FBT002
):
"""Set the background color.
Parameters
----------
color : ColorLike, optional
Either a string, rgb list, or hex color string. Defaults
to current theme parameters. For example:
* ``color='white'``
* ``color='w'``
* ``color=[1.0, 1.0, 1.0]``
* ``color='#FFFFFF'``
top : ColorLike, optional
If given, this will enable a gradient background where the
``color`` argument is at the bottom and the color given in ``top``
will be the color at the top of the renderer.
right : ColorLike, optional
If given, this will enable a gradient background where the
``color`` argument is at the left and the color given in ``right``
will be the color at the right of the renderer.
side : ColorLike, optional
If given, this will enable a gradient background where the
``color`` argument is at the center and the color given in ``side``
will be the color at the side of the renderer.
corner : ColorLike, optional
If given, this will enable a gradient background where the
``color`` argument is at the center and the color given in ``corner``
will be the color at the corner of the renderer.
all_renderers : bool, default: True
If ``True``, applies to all renderers in subplots. If ``False``,
then only applies to the active renderer.
Examples
--------
Set the background color to black.
>>> import pyvista as pv
>>> plotter = pv.Plotter()
>>> plotter.set_background('black')
>>> plotter.background_color
Color(name='black', hex='#000000ff', opacity=255)
>>> plotter.close()
Set the background color at the bottom to black and white at
the top. Display a cone as well.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Cone())
>>> pl.set_background('black', top='white')
>>> pl.show()
"""
if all_renderers:
for renderer in self:
renderer.set_background(color, top=top, right=right, side=side, corner=corner)
self._shadow_renderer.set_background(color) # type: ignore[union-attr]
else:
self.active_renderer.set_background(
color,
top=top,
right=right,
side=side,
corner=corner,
)
@_deprecate_positional_args(allowed=['color_cycler'])
def set_color_cycler(self, color_cycler, all_renderers: bool = True): # noqa: FBT001, FBT002
"""Set or reset the color cycler.
This color cycler is iterated over by each sequential :class:`~pyvista.Plotter.add_mesh`
call to set the default color of the dataset being plotted.
When setting, the value must be either a list of color-like objects,
or a cycler of color-like objects. If the value passed is a single
string, it must be one of:
* ``'default'`` - Use the default color cycler (matches matplotlib's default)
* ``'matplotlib`` - Dynamically get matplotlib's current theme's color cycler.
* ``'all'`` - Cycle through all available colors in
``pyvista.plotting.colors.hexcolors``
Setting to ``None`` will disable the use of the color cycler on this
renderer.
.. note::
If a mesh has scalar data, set ``color=True`` in the call to :meth:`add_mesh`
to color the mesh with the next color in the cycler. Otherwise the mesh's
scalars are used to color the mesh by default.
Parameters
----------
color_cycler : str | cycler.Cycler | sequence[ColorLike]
The colors to cycle through.
all_renderers : bool, default: True
If ``True``, applies to all renderers in subplots. If ``False``,
then only applies to the active renderer.
See Also
--------
:ref:`color_cycler_example`
Examples
--------
Set the default color cycler to iterate through red, green, and blue.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.set_color_cycler(['red', 'green', 'blue'])
>>> _ = pl.add_mesh(pv.Cone(center=(0, 0, 0))) # red
>>> _ = pl.add_mesh(pv.Cube(center=(1, 0, 0))) # green
>>> _ = pl.add_mesh(pv.Sphere(center=(1, 1, 0))) # blue
>>> _ = pl.add_mesh(pv.Cylinder(center=(0, 1, 0))) # red again
>>> pl.show()
"""
if all_renderers:
for renderer in self:
renderer.set_color_cycler(color_cycler)
else:
self.active_renderer.set_color_cycler(color_cycler)
def remove_background_image(self):
"""Remove the background image at the current renderer.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> pl = pv.Plotter(shape=(1, 2))
>>> pl.subplot(0, 0)
>>> actor = pl.add_mesh(pv.Sphere())
>>> pl.add_background_image(examples.mapfile, as_global=False)
>>> pl.subplot(0, 1)
>>> actor = pl.add_mesh(pv.Cube())
>>> pl.add_background_image(examples.mapfile, as_global=False)
>>> pl.remove_background_image()
>>> pl.show()
"""
renderer = self._background_renderers[self.active_index]
if renderer is None:
msg = 'No background image to remove at this subplot'
raise RuntimeError(msg)
renderer.deep_clean()
self._background_renderers[self.active_index] = None
def __del__(self):
"""Destructor."""
self._shadow_renderer = None
@@ -0,0 +1,583 @@
"""PyVista Scalar bar module."""
from __future__ import annotations
import contextlib
import weakref
import numpy as np
import pyvista
from pyvista import MAX_N_COLOR_BARS
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .colors import Color
from .tools import parse_font_family
class ScalarBars(_NoNewAttrMixin):
"""Plotter Scalar Bars.
Parameters
----------
plotter : pyvista.Plotter
Plotter that the scalar bars are associated with.
"""
def __init__(self, plotter):
"""Initialize ScalarBars."""
self._plotter = weakref.proxy(plotter)
self._scalar_bar_ranges = {}
self._scalar_bar_mappers = {}
self._scalar_bar_actors = {}
self._scalar_bar_widgets = {}
def clear(self):
"""Remove all scalar bars and resets all scalar bar properties."""
self._scalar_bar_ranges = {}
self._scalar_bar_mappers = {}
self._scalar_bar_actors = {}
self._scalar_bar_widgets = {}
def __repr__(self):
"""Nice representation of this class."""
lines = []
lines.append('Scalar Bar Title Interactive')
for title in self._scalar_bar_actors:
interactive = title in self._scalar_bar_widgets
title_quotes = f'"{title}"'
lines.append(f'{title_quotes:20} {interactive!s:5}')
return '\n'.join(lines)
@_deprecate_positional_args(allowed=['actor'])
def _remove_mapper_from_plotter(
self,
actor,
reset_camera: bool = False, # noqa: FBT001, FBT002
render: bool = False, # noqa: FBT001, FBT002
): # numpydoc ignore=PR01,RT01
"""Remove an actor's mapper from the given plotter's _scalar_bar_mappers.
This ensures that when actors are removed, their corresponding
scalar bars are removed.
"""
try:
mapper = actor.GetMapper()
except AttributeError:
return
# NOTE: keys to list to prevent iterator changing during loop
for name in list(self._scalar_bar_mappers):
with contextlib.suppress(ValueError):
self._scalar_bar_mappers[name].remove(mapper)
if not self._scalar_bar_mappers[name]:
slot = self._plotter._scalar_bar_slot_lookup.pop(name, None)
if slot is not None:
self._scalar_bar_mappers.pop(name)
self._scalar_bar_ranges.pop(name)
self._plotter.remove_actor(
self._scalar_bar_actors.pop(name),
reset_camera=reset_camera,
render=render,
)
self._plotter._scalar_bar_slots.add(slot)
return
@_deprecate_positional_args(allowed=['title'])
def remove_scalar_bar(self, title=None, render: bool = True): # noqa: FBT001, FBT002
"""Remove a scalar bar.
Parameters
----------
title : str, optional
Title of the scalar bar to remove. Required if there is
more than one scalar bar.
render : bool, default: True
Render upon scalar bar removal. Set this to ``False`` to
stop the render window from rendering when a scalar bar
is removed.
Examples
--------
Remove a scalar bar from a plotter.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh['data'] = mesh.points[:, 2]
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, cmap='coolwarm')
>>> pl.remove_scalar_bar()
>>> pl.show()
"""
if title is None:
if len(self) > 1:
titles = ', '.join(f'"{key}"' for key in self._scalar_bar_actors)
msg = (
'Multiple scalar bars found. Pick title of the'
f'scalar bar from one of the following:\n{titles}'
)
raise ValueError(msg)
else:
title = next(iter(self._scalar_bar_actors.keys()))
actor = self._scalar_bar_actors.pop(title)
self._plotter.remove_actor(actor, render=render)
self._scalar_bar_ranges.pop(title)
self._scalar_bar_mappers.pop(title)
# add back in the scalar bar slot
slot = self._plotter._scalar_bar_slot_lookup.pop(title, None)
if slot is not None:
self._plotter._scalar_bar_slots.add(slot)
widget = self._scalar_bar_widgets.pop(title, None)
if widget is not None:
widget.SetEnabled(0)
def __len__(self):
"""Return the number of scalar bar actors."""
return len(self._scalar_bar_actors)
def __getitem__(self, index):
"""Return a scalar bar actor."""
return self._scalar_bar_actors[index]
def keys(self): # numpydoc ignore=RT01
"""Scalar bar keys."""
return self._scalar_bar_actors.keys()
def values(self): # numpydoc ignore=RT01
"""Scalar bar values."""
return self._scalar_bar_actors.values()
def items(self): # numpydoc ignore=RT01
"""Scalar bar items."""
return self._scalar_bar_actors.items()
def __contains__(self, key) -> bool:
"""Check if a title is a valid actors."""
return key in self._scalar_bar_actors
@_deprecate_positional_args(allowed=['title'])
def add_scalar_bar( # noqa: PLR0917
self,
title='',
mapper=None,
n_labels=5,
italic: bool = False, # noqa: FBT001, FBT002
bold: bool = False, # noqa: FBT001, FBT002
title_font_size=None,
label_font_size=None,
color=None,
font_family=None,
shadow: bool = False, # noqa: FBT001, FBT002
width=None,
height=None,
position_x=None,
position_y=None,
vertical=None,
interactive=None,
fmt=None,
use_opacity: bool = True, # noqa: FBT001, FBT002
outline: bool = False, # noqa: FBT001, FBT002
nan_annotation: bool = False, # noqa: FBT001, FBT002
below_label=None,
above_label=None,
background_color=None,
n_colors=None,
fill: bool = False, # noqa: FBT001, FBT002
render: bool = False, # noqa: FBT001, FBT002
theme=None,
unconstrained_font_size: bool = False, # noqa: FBT001, FBT002
):
"""Create scalar bar using the ranges as set by the last input mesh.
Parameters
----------
title : str, default: ""
Title of the scalar bar. Default is rendered as an empty title.
mapper : :vtk:`vtkMapper`, optional
Mapper used for the scalar bar. Defaults to the last
mapper created by the plotter.
n_labels : int, default: 5
Number of labels to use for the scalar bar.
italic : bool, default: False
Italicises title and bar labels.
bold : bool, default: False
Bolds title and bar labels.
title_font_size : float, optional
Sets the size of the title font. Defaults to ``None`` and is sized
according to :attr:`pyvista.plotting.themes.Theme.font`.
label_font_size : float, optional
Sets the size of the title font. Defaults to ``None`` and is sized
according to :attr:`pyvista.plotting.themes.Theme.font`.
color : ColorLike, optional
Either a string, rgb list, or hex color string. Default
set by :attr:`pyvista.plotting.themes.Theme.font`. Can be
in one of the following formats:
* ``color='white'``
* ``color='w'``
* ``color=[1.0, 1.0, 1.0]``
* ``color='#FFFFFF'``
font_family : {'courier', 'times', 'arial'}
Font family. Default is set by
:attr:`pyvista.plotting.themes.Theme.font`.
shadow : bool, default: False
Adds a black shadow to the text.
width : float, optional
The percentage (0 to 1) width of the window for the colorbar.
Default set by
:attr:`pyvista.plotting.themes.Theme.colorbar_vertical` or
:attr:`pyvista.plotting.themes.Theme.colorbar_horizontal`
depending on the value of ``vertical``.
height : float, optional
The percentage (0 to 1) height of the window for the
colorbar. Default set by
:attr:`pyvista.plotting.themes.Theme.colorbar_vertical` or
:attr:`pyvista.plotting.themes.Theme.colorbar_horizontal`
depending on the value of ``vertical``.
position_x : float, optional
The percentage (0 to 1) along the windows's horizontal
direction to place the bottom left corner of the colorbar.
Default set by
:attr:`pyvista.plotting.themes.Theme.colorbar_vertical` or
:attr:`pyvista.plotting.themes.Theme.colorbar_horizontal`
depending on the value of ``vertical``.
position_y : float, optional
The percentage (0 to 1) along the windows's vertical
direction to place the bottom left corner of the colorbar.
Default set by
:attr:`pyvista.plotting.themes.Theme.colorbar_vertical` or
:attr:`pyvista.plotting.themes.Theme.colorbar_horizontal`
depending on the value of ``vertical``.
vertical : bool, optional
Use vertical or horizontal scalar bar. Default set by
:attr:`pyvista.plotting.themes.Theme.colorbar_orientation`.
interactive : bool, optional
Use a widget to control the size and location of the scalar bar.
Default set by :attr:`pyvista.plotting.themes.Theme.interactive`.
fmt : str, optional
``printf`` format for labels.
Default set by :attr:`pyvista.plotting.themes.Theme.font`.
use_opacity : bool, default: True
Optionally display the opacity mapping on the scalar bar.
outline : bool, default: False
Optionally outline the scalar bar to make opacity mappings more
obvious.
nan_annotation : bool, default: False
Annotate the NaN color.
below_label : str, optional
String annotation for values below the scalars range.
above_label : str, optional
String annotation for values above the scalars range.
background_color : ColorLike, optional
The color used for the background in RGB format.
n_colors : int, optional
The maximum number of color displayed in the scalar bar.
fill : bool, default: False
Draw a filled box behind the scalar bar with the
``background_color``.
render : bool, default: False
Force a render when True.
theme : pyvista.plotting.themes.Theme, optional
Plot-specific theme. By default, calling from the
``Plotter``, will use the plotter theme. Setting to
``None`` will use the global theme.
unconstrained_font_size : bool, default: False
Whether the font size of title and labels is unconstrained.
When it is constrained, the size of the scalar bar will constrain the font size.
When it is not, the size of the font will always be respected.
Using custom labels will force this to be ``True``.
.. versionadded:: 0.44.0
Returns
-------
:vtk:`vtkScalarBarActor`
Scalar bar actor.
Notes
-----
Setting ``title_font_size``, or ``label_font_size`` disables
automatic font sizing for both the title and label.
See Also
--------
:ref:`scalar_bar_example`
Examples
--------
Add a custom interactive scalar bar that is horizontal, has an
outline, and has a custom formatting.
>>> import pyvista as pv
>>> sphere = pv.Sphere()
>>> sphere['Data'] = sphere.points[:, 2]
>>> plotter = pv.Plotter()
>>> _ = plotter.add_mesh(sphere, show_scalar_bar=False)
>>> _ = plotter.add_scalar_bar(
... 'Data',
... interactive=True,
... vertical=False,
... title_font_size=35,
... label_font_size=30,
... outline=True,
... fmt='%10.5f',
... )
>>> plotter.show()
"""
if mapper is None:
msg = 'Mapper cannot be ``None`` when creating a scalar bar'
raise ValueError(msg)
if theme is None:
theme = pyvista.global_theme
if interactive is None:
interactive = theme.interactive
if font_family is None:
font_family = theme.font.family
if label_font_size is None:
label_font_size = theme.font.label_size
if title_font_size is None:
title_font_size = theme.font.title_size
if fmt is None:
fmt = theme.font.fmt
if vertical is None and theme.colorbar_orientation.lower() == 'vertical':
vertical = True
# Automatically choose size if not specified
if width is None:
width = theme.colorbar_vertical.width if vertical else theme.colorbar_horizontal.width
if height is None:
if vertical:
height = theme.colorbar_vertical.height
else:
height = theme.colorbar_horizontal.height
# Check that this data hasn't already been plotted
if title in list(self._scalar_bar_ranges.keys()):
clim = list(self._scalar_bar_ranges[title])
newrng = mapper.scalar_range
oldmappers = self._scalar_bar_mappers[title]
# get max for range and reset everything
clim[0] = min(newrng[0], clim[0])
clim[1] = max(newrng[1], clim[1])
for mh in oldmappers:
mh.scalar_range = clim[0], clim[1]
mapper.scalar_range = clim[0], clim[1]
self._scalar_bar_mappers[title].append(mapper)
self._scalar_bar_ranges[title] = clim
self._scalar_bar_actors[title].SetLookupTable(mapper.lookup_table)
# Color bar already present and ready to be used so returning
return None
# Automatically choose location if not specified
if position_x is None or position_y is None:
if not self._plotter._scalar_bar_slots:
msg = f'Maximum number of color bars ({MAX_N_COLOR_BARS}) reached.'
raise RuntimeError(msg)
slot = min(self._plotter._scalar_bar_slots)
self._plotter._scalar_bar_slots.remove(slot)
self._plotter._scalar_bar_slot_lookup[title] = slot
if position_x is None:
if vertical:
position_x = theme.colorbar_vertical.position_x
position_x -= slot * (width + 0.2 * width)
else:
position_x = theme.colorbar_horizontal.position_x
if position_y is None:
if vertical:
position_y = theme.colorbar_vertical.position_y
else:
position_y = theme.colorbar_horizontal.position_y
position_y += slot * height
# parse color
color = Color(color, default_color=theme.font.color)
# Create scalar bar
scalar_bar = _vtk.vtkScalarBarActor()
# self._scalar_bars.append(scalar_bar)
if background_color is not None:
background_color = np.array(Color(background_color).int_rgba)
scalar_bar.GetBackgroundProperty().SetColor(background_color[0:3])
if fill:
scalar_bar.DrawBackgroundOn()
lut = pyvista.LookupTable()
lut.DeepCopy(mapper.lookup_table)
ctable = _vtk.vtk_to_numpy(lut.GetTable())
alphas = ctable[:, -1][:, np.newaxis] / 255.0
use_table = ctable.copy()
use_table[:, -1] = 255.0
ctable = (use_table * alphas) + background_color * (1 - alphas)
lut.SetTable(_vtk.numpy_to_vtk(ctable, array_type=_vtk.VTK_UNSIGNED_CHAR))
else:
lut = mapper.lookup_table
scalar_bar.SetLookupTable(lut)
if n_colors is None:
# ensure the number of colors in the scalarbar's lookup table is at
# least the number in the mapper
n_colors = mapper.lookup_table.n_values
scalar_bar.SetMaximumNumberOfColors(n_colors)
if n_labels < 1:
scalar_bar.SetDrawTickLabels(False)
else:
scalar_bar.SetDrawTickLabels(True)
scalar_bar.SetNumberOfLabels(n_labels)
if nan_annotation:
scalar_bar.DrawNanAnnotationOn()
if above_label is not None:
scalar_bar.DrawAboveRangeSwatchOn()
scalar_bar.SetAboveRangeAnnotation(above_label)
elif lut.above_range_color:
scalar_bar.DrawAboveRangeSwatchOn()
scalar_bar.SetAboveRangeAnnotation('above')
if below_label is not None:
scalar_bar.DrawBelowRangeSwatchOn()
scalar_bar.SetBelowRangeAnnotation(below_label)
elif lut.below_range_color:
scalar_bar.DrawBelowRangeSwatchOn()
scalar_bar.SetBelowRangeAnnotation('below')
# edit the size of the colorbar
scalar_bar.SetHeight(height)
scalar_bar.SetWidth(width)
scalar_bar.SetPosition(position_x, position_y)
if fmt is not None:
scalar_bar.SetLabelFormat(fmt)
if vertical:
scalar_bar.SetOrientationToVertical()
else:
scalar_bar.SetOrientationToHorizontal()
if label_font_size is not None or title_font_size is not None:
scalar_bar.SetUnconstrainedFontSize(True)
scalar_bar.SetAnnotationTextScaling(False)
else:
scalar_bar.SetAnnotationTextScaling(True)
label_text = scalar_bar.GetLabelTextProperty()
anno_text = scalar_bar.GetAnnotationTextProperty()
label_text.SetColor(color.float_rgb)
anno_text.SetColor(color.float_rgb)
label_text.SetShadow(shadow)
anno_text.SetShadow(shadow)
# Set font
label_text.SetFontFamily(parse_font_family(font_family))
anno_text.SetFontFamily(parse_font_family(font_family))
label_text.SetItalic(italic)
anno_text.SetItalic(italic)
label_text.SetBold(bold)
anno_text.SetBold(bold)
if label_font_size:
label_text.SetFontSize(label_font_size)
anno_text.SetFontSize(label_font_size)
# Set properties
self._scalar_bar_ranges[title] = mapper.scalar_range
self._scalar_bar_mappers[title] = [mapper]
scalar_bar.SetTitle(title)
title_text = scalar_bar.GetTitleTextProperty()
title_text.SetJustificationToCentered()
title_text.SetItalic(italic)
title_text.SetBold(bold)
title_text.SetShadow(shadow)
if title_font_size:
title_text.SetFontSize(title_font_size)
# Set font
title_text.SetFontFamily(parse_font_family(font_family))
# set color
title_text.SetColor(color.float_rgb)
self._scalar_bar_actors[title] = scalar_bar
if interactive:
scalar_widget = _vtk.vtkScalarBarWidget()
scalar_widget.SetScalarBarActor(scalar_bar)
scalar_widget.SetInteractor(self._plotter.iren.interactor)
scalar_widget.SetEnabled(1)
rep = scalar_widget.GetRepresentation()
scalar_widget.On()
if vertical is True or vertical is None:
rep.SetOrientation(1) # type: ignore[attr-defined] # 0 = Horizontal, 1 = Vertical
else:
# y position determined empirically
y = -position_y / 2 - height - scalar_bar.GetPosition()[1]
rep.GetPositionCoordinate().SetValue(width, y) # type: ignore[attr-defined]
rep.GetPosition2Coordinate().SetValue(height, width) # type: ignore[attr-defined]
rep.SetOrientation(0) # type: ignore[attr-defined] # 0 = Horizontal, 1 = Vertical
self._scalar_bar_widgets[title] = scalar_widget
if use_opacity:
scalar_bar.SetUseOpacity(True)
if outline:
scalar_bar.SetDrawFrame(True)
frame_prop = scalar_bar.GetFrameProperty()
frame_prop.SetColor(color.float_rgb)
else:
scalar_bar.SetDrawFrame(False)
if unconstrained_font_size:
scalar_bar.SetUnconstrainedFontSize(True)
# finally, add to the actor and return the scalar bar
self._plotter.add_actor(scalar_bar, reset_camera=False, pickable=False, render=render)
return scalar_bar
@@ -0,0 +1,874 @@
"""Contains the pyvista.Text class."""
from __future__ import annotations
import pathlib
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Literal
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core._typing_core import BoundsTuple
from pyvista.core.utilities.misc import _check_range
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .colors import Color
from .prop3d import _Prop3DMixin
from .themes import Theme
from .tools import FONTS
if TYPE_CHECKING:
from collections.abc import Sequence
from pyvista.core._typing_core import VectorLike
from ._typing import ColorLike
HorizontalOptions = Literal['left', 'center', 'right']
VerticalOptions = Literal['bottom', 'center', 'top']
class CornerAnnotation(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _NameMixin, _vtk.vtkCornerAnnotation
):
"""Text annotation in four corners.
This is an annotation object that manages four text actors / mappers to provide
annotation in the four corners of a viewport.
Parameters
----------
position : str | bool
Position of the text.
text : str
Text input.
prop : pyvista.TextProperty, optional
Text property.
linear_font_scale_factor : float, optional
Linear font scale factor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
Examples
--------
Create text annotation in four corners.
>>> from pyvista import CornerAnnotation
>>> text = CornerAnnotation(0, 'text')
>>> prop = text.prop
"""
@_deprecate_positional_args(allowed=['position', 'text'])
def __init__( # noqa: PLR0917
self, position, text, prop=None, linear_font_scale_factor=None, name=None
):
"""Initialize a new text annotation descriptor."""
super().__init__()
self.set_text(position, text)
if prop is None:
self.prop = TextProperty()
if linear_font_scale_factor is not None:
self.linear_font_scale_factor = linear_font_scale_factor
self._name = name
def get_text(self, position):
"""Get the text to be displayed for each corner.
Parameters
----------
position : str | bool
Position of the text.
Returns
-------
str
Text to be displayed for each corner.
"""
return self.GetText(position)
def set_text(self, position, text):
"""Set the text to be displayed for each corner.
Parameters
----------
position : str | bool
Position of the text.
text : str
Text to be displayed for each corner.
"""
corner_mappings = {
'lower_left': self.LowerLeft,
'lower_right': self.LowerRight,
'upper_left': self.UpperLeft,
'upper_right': self.UpperRight,
'lower_edge': self.LowerEdge,
'upper_edge': self.UpperEdge,
'left_edge': self.LeftEdge,
'right_edge': self.RightEdge,
}
corner_mappings['ll'] = corner_mappings['lower_left']
corner_mappings['lr'] = corner_mappings['lower_right']
corner_mappings['ul'] = corner_mappings['upper_left']
corner_mappings['ur'] = corner_mappings['upper_right']
corner_mappings['top'] = corner_mappings['upper_edge']
corner_mappings['bottom'] = corner_mappings['lower_edge']
corner_mappings['right'] = corner_mappings['right_edge']
corner_mappings['r'] = corner_mappings['right_edge']
corner_mappings['left'] = corner_mappings['left_edge']
corner_mappings['l'] = corner_mappings['left_edge']
if isinstance(position, str):
position = corner_mappings[position]
elif position is True:
position = corner_mappings['upper_left']
self.SetText(position, text)
@property
def prop(self) -> TextProperty:
"""Property of this actor.
Returns
-------
pyvista.TextProperty
Property of this actor.
"""
return self.GetTextProperty()
@prop.setter
def prop(self, prop: TextProperty):
self.SetTextProperty(prop)
@property
def linear_font_scale_factor(self) -> float:
"""Font scaling factors.
Returns
-------
float
Font scaling factors.
"""
return self.GetLinearFontScaleFactor()
@linear_font_scale_factor.setter
def linear_font_scale_factor(self, factor: float):
self.SetLinearFontScaleFactor(factor)
class Text(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _NameMixin, _vtk.vtkTextActor):
r"""Define text by default theme.
Parameters
----------
text : str, optional
Text string to be displayed.
"\n" is recognized as a carriage return/linefeed (line separator).
The characters must be in the UTF-8 encoding.
position : Sequence[float], optional
The position coordinate.
prop : pyvista.TextProperty, optional
The property of this actor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
Examples
--------
Create a text with text's property.
>>> from pyvista import Text
>>> text = Text()
>>> prop = text.prop
"""
@_deprecate_positional_args(allowed=['text'])
def __init__( # noqa: PLR0917
self, text=None, position=None, prop=None, name=None
):
"""Initialize a new text descriptor."""
super().__init__()
if text is not None:
self.input = text
if position is not None:
self.position = position
if prop is None:
self.prop = TextProperty()
self._name = name
@property
def input(self):
r"""Text string to be displayed.
Returns
-------
str
Text string to be displayed.
"\n" is recognized as a carriage return/linefeed (line separator).
The characters must be in the UTF-8 encoding.
"""
return self.GetInput()
@input.setter
def input(self, text: str):
self.SetInput(text)
@property
def prop(self):
"""Property of this actor.
Returns
-------
pyvista.TextProperty
Property of this actor.
"""
return self.GetTextProperty()
@prop.setter
def prop(self, prop: TextProperty):
self.SetTextProperty(prop)
@property
def position(self):
"""Position coordinate.
Returns
-------
Sequence[float]
Position coordinate.
"""
return self.GetPosition()
@position.setter
def position(self, position: Sequence[float]):
self.SetPosition(position[0], position[1])
class Label(_Prop3DMixin, Text):
"""2D label actor with a 3D position coordinate.
Unlike :class:`~pyvista.Text`, which uses 2D viewport coordinates to position text
in a plot, this class instead uses a 3D position coordinate. This class may be
positioned, oriented, and transformed in a manner similar to a 3D
:class:`~pyvista.Actor`.
In addition, this class supports an additional :attr:`relative_position` attribute.
In general, it is recommended to simply use :attr:`~pyvista.Prop3D.position` when positioning a
:class:`Label` by itself. However, if the position of the label depends on the
positioning of another actor, both :attr:`~pyvista.Prop3D.position` and
:attr:`relative_position` may be used together.
In these cases, the :attr:`~pyvista.Prop3D.position` of the label and actor
should be kept in-sync. See the examples below.
Parameters
----------
text : str, optional
Text string to be displayed.
position : VectorLike[float]
Position of the text in XYZ coordinates.
relative_position : VectorLike[float]
Position of the text in XYZ coordinates relative to its :attr:`~pyvista.Prop3D.position`.
size : int
Size of the text label.
prop : pyvista.TextProperty, optional
The property of this actor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
See Also
--------
pyvista.Plotter.add_point_labels
Examples
--------
Create a label for a point of interest. Here we add a label to the tip of a cone.
>>> import pyvista as pv
>>> cone_dataset = pv.Cone()
>>> tip = (0.5, 0, 0)
>>> label = pv.Label('tip', position=tip)
Plot the mesh and label.
>>> pl = pv.Plotter()
>>> cone_actor = pl.add_mesh(cone_dataset)
>>> _ = pl.add_actor(label)
>>> pl.show()
The previous example set the label's position as the cone's tip explicitly.
However, this means that the two actors now have different positions.
>>> cone_actor.position
(0.0, 0.0, 0.0)
>>> label.position
(0.5, 0.0, 0.0)
And if we change the 3D orientation of the cone and label, the label is no longer
positioned at the tip.
>>> cone_actor.orientation = 0, 0, 90
>>> label.orientation = 0, 0, 90
>>>
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> pl.show()
This is because rotations by :class:`pyvista.Prop3D` are applied **before** the
actor is moved to its final position, and therefore the label's position is not
considered in the rotation. Hence, the final position of the label remains at
``(0.5, 0.0, 0.0)`` as it did earlier, despite changing its orientation.
If we want the position of the label to have the same positioning *relative* to the
cone, we can instead set its :attr:`relative_position`.
First, reset the label's position to match the cone's position.
>>> label.position = cone_actor.position
>>> label.position
(0.0, 0.0, 0.0)
Now set its :attr:`relative_position` to the tip of the cone.
>>> label.relative_position = tip
>>> label.relative_position
(0.5, 0.0, 0.0)
Plot the results. The label is now correctly positioned at the tip of the cone.
This is because the :attr:`relative_position` is considered as part of the
rotation.
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> pl.show()
As long as the label and cone's :class:`pyvista.Prop3D` attributes are modified
together and synchronized, the label will remain at the tip of the cone.
Modify the position of the label and tip.
>>> cone_actor.position = (1.0, 2.0, 3.0)
>>> label.position = (1.0, 2.0, 3.0)
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> _ = pl.add_axes_at_origin()
>>> pl.show()
"""
def __init__(
self,
text: str | None = None,
position: VectorLike[float] = (0.0, 0.0, 0.0),
relative_position: VectorLike[float] = (0.0, 0.0, 0.0),
*,
size: int = 50,
prop: pyvista.Property | None = None,
name: str = 'Label',
):
Text.__init__(self, text=text, prop=prop)
self.GetPositionCoordinate().SetCoordinateSystemToWorld()
self.SetTextScaleModeToNone() # Use font size to control size of text
self._name = name
_Prop3DMixin.__init__(self)
self.relative_position = relative_position
self.position = position
self.size = size
@property
def _label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label in xyz space.
This is the "true" position of the label. Internally this is loosely
equal to :attr:`~pyvista.Prop3D.position` + :attr:`relative_position`.
"""
return self.GetPositionCoordinate().GetValue()
@_label_position.setter
def _label_position(self, position: VectorLike[float]):
valid_position = _validation.validate_array3(position)
self.GetPositionCoordinate().SetValue(valid_position)
@property
def size(self) -> int: # numpydoc ignore=RT01
"""Size of the text label.
Notes
-----
The text property's font size used to control the size of the label.
"""
return self.prop.font_size
@size.setter
def size(self, size: int):
self.prop.font_size = size
@property
def relative_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label relative to its :attr:`~pyvista.Prop3D.position`."""
return tuple(self._relative_position.tolist())
@relative_position.setter
def relative_position(self, position: VectorLike[float]):
self._relative_position = _validation.validate_array3(position, dtype_out=float)
self._post_set_update()
def _post_set_update(self):
# Update the label's underlying text position
matrix4x4 = self._transformation_matrix
vector4 = (*self.relative_position, 1)
new_position = (matrix4x4 @ vector4)[:3]
self._label_position = new_position
def _get_bounds(self) -> BoundsTuple:
# Define its 3D position as its bounds
x, y, z = self._label_position
return BoundsTuple(x, x, y, y, z, z)
class TextProperty(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkTextProperty):
"""Define text's property.
Parameters
----------
theme : pyvista.plotting.themes.Theme, optional
Plot-specific theme.
color : pyvista.ColorLike, optional
Either a string, RGB list, or hex color string. For example:
``color='white'``, ``color='w'``, ``color=[1.0, 1.0, 1.0]``, or
``color='#FFFFFF'``. Color will be overridden if scalars are
specified.
font_family : str | None, optional
Font family or None.
orientation : float, optional
Text's orientation (in degrees).
font_size : int, optional
Font size.
font_file : str, optional
Font file path.
shadow : bool, optional
If enable the shadow.
justification_horizontal : str, optional
Text's horizontal justification.
Should be either "left", "center" or "right".
justification_vertical : str, optional
Text's vertical justification.
Should be either "bottom", "center" or "top".
italic : bool, default: False
Italicises title and bar labels.
bold : bool, default: True
Bolds title and bar labels.
background_color : pyvista.Color, optional
Background color of text.
background_opacity : pyvista.Color, optional
Background opacity of text.
Examples
--------
Create a text's property.
>>> from pyvista import TextProperty
>>> prop = TextProperty()
>>> prop.opacity = 0.5
>>> prop.background_color = 'b'
>>> prop.background_opacity = 0.5
>>> prop.show_frame = True
>>> prop.frame_color = 'b'
>>> prop.frame_width = 10
>>> prop.frame_color
Color(name='blue', hex='#0000ffff', opacity=255)
"""
_theme = Theme()
_color_set = None
_background_color_set = None
_font_family = None
@_deprecate_positional_args(allowed=['theme'])
def __init__( # noqa: PLR0917
self,
theme=None,
color=None,
font_family=None,
orientation=None,
font_size=None,
font_file=None,
shadow: bool = False, # noqa: FBT001, FBT002
justification_horizontal=None,
justification_vertical=None,
italic: bool = False, # noqa: FBT001, FBT002
bold: bool = False, # noqa: FBT001, FBT002
background_color=None,
background_opacity=None,
):
"""Initialize text's property."""
super().__init__()
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.color = color
self.font_family = font_family
if orientation is not None:
self.orientation = orientation
if font_size is not None:
self.font_size = font_size
if font_file is not None:
self.set_font_file(font_file)
if shadow:
self.enable_shadow()
if justification_horizontal is not None:
self.justification_horizontal = justification_horizontal
if justification_vertical is not None:
self.justification_vertical = justification_vertical
self.italic = italic
self.bold = bold
if background_color is not None:
self.background_color = background_color
if background_opacity is not None:
self.background_opacity = background_opacity
@property
def color(self) -> Color:
"""Color of text's property.
Returns
-------
pyvista.Color
Color of text's property.
"""
return Color(self.GetColor())
@color.setter
def color(self, color: ColorLike):
self._color_set = color is not None
rgb_color = Color(color, default_color=self._theme.font.color)
self.SetColor(rgb_color.float_rgb)
@property
def opacity(self) -> float:
"""Opacity of text's property.
Returns
-------
float
Opacity of the text. A single float value that will be applied globally
opacity of the text and uniformly applied everywhere. Between 0 and 1.
"""
return self.GetOpacity()
@opacity.setter
def opacity(self, opacity: float):
_check_range(opacity, (0, 1), 'opacity')
self.SetOpacity(opacity)
@property
def background_color(self) -> Color:
"""Background color of text's property.
Returns
-------
pyvista.Color
Background color of text's property.
"""
return Color(self.GetBackgroundColor())
@background_color.setter
def background_color(self, color: ColorLike):
self._background_color_set = color is not None
rgb_color = Color(color)
self.SetBackgroundColor(rgb_color.float_rgb)
@property
def background_opacity(self) -> float:
"""Background opacity of text's property.
Returns
-------
float
Background opacity of the text. A single float value that will be applied globally.
Background opacity of the text and uniformly applied everywhere. Between 0 and 1.
"""
return self.GetBackgroundOpacity()
@background_opacity.setter
def background_opacity(self, opacity: float):
_check_range(opacity, (0, 1), 'background_opacity')
self.SetBackgroundOpacity(opacity)
@property
def show_frame(self) -> bool:
"""Visibility of frame.
Returns
-------
bool:
If shows the frame.
"""
return bool(self.GetFrame())
@show_frame.setter
def show_frame(self, frame: bool):
self.SetFrame(frame)
@property
def frame_color(self) -> Color:
"""Frame color of text property.
Returns
-------
pyvista.Color
Frame color of text property.
"""
return Color(self.GetFrameColor())
@frame_color.setter
def frame_color(self, color):
self.SetFrameColor(Color(color).float_rgb)
@property
def frame_width(self) -> int:
"""Width of the frame.
Returns
-------
int
Width of the frame. The width is expressed in pixels.
The default is 1 pixel.
"""
return self.GetFrameWidth()
@frame_width.setter
def frame_width(self, width: int):
self.SetFrameWidth(width)
@property
def font_family(self) -> str | None:
"""Font family.
Returns
-------
str | None
Font family or None.
"""
return self._font_family
@font_family.setter
def font_family(self, font_family: str | None):
if font_family is None:
font_family = self._theme.font.family
self._font_family = font_family
self.SetFontFamily(FONTS[self._font_family].value)
@property
def font_size(self) -> int:
"""Font size.
Returns
-------
int
Font size.
"""
return self.GetFontSize()
@font_size.setter
def font_size(self, font_size: int):
self.SetFontSize(font_size)
def enable_shadow(self) -> None:
"""Enable the shadow."""
self.SetShadow(True)
@property
def orientation(self) -> float:
"""Text's orientation (in degrees).
Returns
-------
float
Text's orientation (in degrees).
"""
return self.GetOrientation()
@orientation.setter
def orientation(self, orientation: float):
self.SetOrientation(orientation)
def set_font_file(self, font_file: str):
"""Set the font file.
Parameters
----------
font_file : str
Font file path.
"""
path = pathlib.Path(font_file)
path = path.resolve()
if not Path(path).is_file():
msg = f'Unable to locate {path}'
raise FileNotFoundError(msg)
self.SetFontFamily(_vtk.VTK_FONT_FILE)
self.SetFontFile(str(path))
@property
def justification_horizontal(self) -> str:
"""Text's justification horizontal.
Returns
-------
str
Text's horizontal justification.
Should be either "left", "center" or "right".
"""
justification = self.GetJustificationAsString().lower()
if justification == 'centered':
justification = 'center'
return justification
@justification_horizontal.setter
def justification_horizontal(self, justification: str):
if justification.lower() == 'left':
self.SetJustificationToLeft()
elif justification.lower() == 'center':
self.SetJustificationToCentered()
elif justification.lower() == 'right':
self.SetJustificationToRight()
else:
msg = (
f'Invalid {justification} for justification_horizontal. '
'Should be either "left", "center" or "right".'
)
raise ValueError(msg)
@property
def justification_vertical(self) -> str:
"""Text's vertical justification.
Returns
-------
str
Text's vertical justification.
Should be either "bottom", "center" or "top".
"""
justification = self.GetVerticalJustificationAsString().lower()
if justification == 'centered':
justification = 'center'
return justification
@justification_vertical.setter
def justification_vertical(self, justification: str):
if justification.lower() == 'bottom':
self.SetVerticalJustificationToBottom()
elif justification.lower() == 'center':
self.SetVerticalJustificationToCentered()
elif justification.lower() == 'top':
self.SetVerticalJustificationToTop()
else:
msg = (
f'Invalid {justification} for justification_vertical. '
'Should be either "bottom", "center" or "top".'
)
raise ValueError(msg)
@property
def italic(self) -> bool:
"""Italic of text's property.
Returns
-------
bool
If text is italic.
"""
return bool(self.GetItalic())
@italic.setter
def italic(self, italic: bool):
self.SetItalic(italic)
@property
def bold(self) -> bool:
"""Bold of text's property.
Returns
-------
bool
If text is bold.
"""
return bool(self.GetBold())
@bold.setter
def bold(self, bold: bool):
self.SetBold(bold)
def shallow_copy(self, to_copy: TextProperty) -> None:
"""Create a shallow copy of the text's property.
Parameters
----------
to_copy : pyvista.TextProperty
Text's property to copy from.
"""
self.ShallowCopy(to_copy)
@@ -0,0 +1,696 @@
"""Wrapper for :vtk:`vtkTexture`."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import warnings
import numpy as np
import pyvista
from pyvista.core.dataobject import DataObject
from pyvista.core.utilities.fileio import _try_imageio_imread
from pyvista.core.utilities.misc import AnnotatedIntEnum
from . import _vtk
if TYPE_CHECKING:
from pyvista.core._typing_core import NumpyArray
class Texture(DataObject, _vtk.vtkTexture):
"""Wrap :vtk:`vtkTexture`.
Textures can be used to apply images to surfaces, as in the case of
:ref:`texture_example`.
They can also be used for environment textures to affect the lighting of
the scene, or even as a environment cubemap as in the case of
:ref:`pbr_example` and :ref:`planets_example`.
Parameters
----------
uinput : str, :vtk:`vtkImageData`, :vtk:`vtkTexture`, sequence[ImageData], optional
Filename, :vtk:`vtkImageData`, :vtk:`vtkTexture`, :class:`numpy.ndarray` or a
sequence of images to create a cubemap. If a sequence of images, must
be of the same size and in the following order:
* +X
* -X
* +Y
* -Y
* +Z
* -Z
**kwargs : dict, optional
Optional arguments when reading from a file. Generally unused.
Examples
--------
Load a texture from file. File should be a "image" or "image-like" file.
>>> from pathlib import Path
>>> import pyvista as pv
>>> from pyvista import examples
>>> path = examples.download_masonry_texture(load=False)
>>> Path(path).name
'masonry.bmp'
>>> texture = pv.Texture(path)
>>> texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 256, 256
Create a texture from an RGB array. Note how this is colored per "point"
rather than per "pixel".
>>> import numpy as np
>>> arr = np.array(
... [
... [255, 255, 255],
... [255, 0, 0],
... [0, 255, 0],
... [0, 0, 255],
... ],
... dtype=np.uint8,
... )
>>> arr = arr.reshape((2, 2, 3))
>>> texture = pv.Texture(arr)
>>> texture.plot()
Create a cubemap from 6 images.
>>> px = examples.download_sky(direction='posx') # doctest:+SKIP
>>> nx = examples.download_sky(direction='negx') # doctest:+SKIP
>>> py = examples.download_sky(direction='posy') # doctest:+SKIP
>>> ny = examples.download_sky(direction='negy') # doctest:+SKIP
>>> pz = examples.download_sky(direction='posz') # doctest:+SKIP
>>> nz = examples.download_sky(direction='negz') # doctest:+SKIP
>>> texture = pv.Texture([px, nx, py, ny, pz, nz]) # doctest:+SKIP
>>> texture.cube_map # doctest:+SKIP
True
"""
class WrapType(AnnotatedIntEnum):
"""Types of wrapping a texture can support.
Wrap mode for the texture coordinates valid values are:
* CLAMP_TO_EDGE
* REPEAT (Default in :class:`pyvista.Texture`)
* MIRRORED_REPEAT
* CLAMP_TO_BORDER
See :attr:`Texture.wrap` for usage.
"""
CLAMP_TO_EDGE = (0, 'Clamp to edge')
REPEAT = (1, 'Repeat')
MIRRORED_REPEAT = (2, 'Mirrored repeat')
CLAMP_TO_BORDER = (3, 'Clamp to border')
def __init__(self, uinput=None, **kwargs):
"""Initialize the texture."""
super().__init__(uinput)
if isinstance(uinput, _vtk.vtkTexture):
self._from_texture(uinput)
elif isinstance(uinput, np.ndarray):
self._from_array(uinput)
elif isinstance(uinput, _vtk.vtkImageData):
self._from_image_data(uinput)
elif isinstance(uinput, str):
self._from_file(filename=uinput, **kwargs)
elif isinstance(uinput, Sequence) and len(uinput) == 6:
# Create a cubemap
self.mipmap = True
self.interpolate = True
self.cube_map = True # Must be set prior to setting images
# add each image to the cubemap
for i, image in enumerate(uinput):
if not isinstance(image, pyvista.ImageData):
msg = (
'If a sequence, the each item in the first argument must be a '
'pyvista.ImageData'
)
raise TypeError(msg)
# must flip y for cubemap to display properly
self.SetInputDataObject(i, image._flip_uniform(1))
elif uinput is None:
pass
else:
msg = f'Cannot create a pyvista.Texture from ({type(uinput)})'
raise TypeError(msg)
def _from_file(self, filename, **kwargs):
try:
image = pyvista.read(filename, **kwargs)
if image.n_points < 2: # pragma: no cover
msg = 'Problem reading the image with VTK.'
raise RuntimeError(msg)
self._from_image_data(image)
except (KeyError, ValueError, OSError):
self._from_array(_try_imageio_imread(filename)) # pragma: no cover
def _from_texture(self, texture):
image = texture.GetInput()
self._from_image_data(image)
@property
def interpolate(self) -> bool: # numpydoc ignore=RT01
"""Return if interpolate is enabled or disabled.
Examples
--------
Show the masonry texture without interpolation. Here, we zoom to show
the individual pixels.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.interpolate = False
>>> texture.plot(cpos='xy', zoom=3)
Plot the same texture with interpolation.
>>> texture.interpolate = True
>>> texture.plot(cpos='xy', zoom=3)
"""
return bool(self.GetInterpolate())
@interpolate.setter
def interpolate(self, value: bool):
self.SetInterpolate(value)
@property
def mipmap(self) -> bool: # numpydoc ignore=RT01
"""Return if mipmap is enabled or disabled."""
return bool(self.GetMipmap())
@mipmap.setter
def mipmap(self, value: bool):
self.SetMipmap(value)
def _from_image_data(self, image):
if not isinstance(image, pyvista.ImageData):
image = pyvista.ImageData(image)
self.SetInputDataObject(image)
self.Update()
def _from_array(self, image):
"""Create a texture from a np.ndarray."""
if image.ndim not in [2, 3]:
# we support 2 [single component image] or 3 [e.g. rgb or rgba] dims
msg = 'Input image must be nn by nm by RGB[A]'
raise ValueError(msg)
if image.ndim == 3:
if image.shape[2] not in [1, 3, 4]:
msg = 'Third dimension of the array must be of size 3 (RGB) or 4 (RGBA)'
raise ValueError(msg)
n_components = image.shape[2]
elif image.ndim == 2:
n_components = 1
grid = pyvista.ImageData(dimensions=(image.shape[1], image.shape[0], 1))
grid.point_data['Image'] = np.flip(image.swapaxes(0, 1), axis=1).reshape(
(-1, n_components),
order='F',
)
grid.set_active_scalars('Image')
self._from_image_data(grid)
@property
def repeat(self) -> bool: # numpydoc ignore=RT01
"""Repeat the texture.
This is provided for convenience and backwards compatibility.
For new code, use :func:`Texture.wrap`.
Examples
--------
Load the masonry texture and create a simple :class:`pyvista.PolyData`
with texture coordinates using :func:`pyvista.Plane`. By default the
texture coordinates are between 0 and 1. Let's raise these values over
1 by multiplying them in place. This will allow us to wrap the texture.
>>> import pyvista as pv
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates *= 2
This is the texture plotted with repeat set to ``False``.
>>> texture.repeat = False
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
This is the texture plotted with repeat set to ``True``.
>>> texture.repeat = True
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
return bool(self.GetRepeat())
@repeat.setter
def repeat(self, flag: bool):
self.SetRepeat(flag)
def flip_x(self) -> Texture:
"""Flip the texture in the x direction.
Returns
-------
pyvista.Texture
Flipped texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> flipped = texture.flip_x()
>>> flipped.plot()
"""
return Texture(self.to_image()._flip_uniform(0)) # type: ignore[abstract]
def flip_y(self) -> Texture:
"""Flip the texture in the y direction.
Returns
-------
pyvista.Texture
Flipped texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> flipped = texture.flip_y()
>>> flipped.plot()
"""
return Texture(self.to_image()._flip_uniform(1)) # type: ignore[abstract]
def to_image(self):
"""Return the texture as an image.
Returns
-------
pyvista.ImageData
Texture represented as a uniform grid.
"""
return self.GetInput()
def to_array(self) -> NumpyArray[float]:
"""Return the texture as an array.
Notes
-----
The shape of the array's first two dimensions will be swapped. For
example, a ``(300, 200)`` image will return an array of ``(200, 300)``.
Returns
-------
numpy.ndarray
Texture as a numpy array.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 1600, 1200
>>> texture.to_array().shape
(1200, 1600, 3)
>>> texture.to_array().dtype
dtype('uint8')
"""
return self.to_image().active_scalars.reshape(
[*list(self.dimensions)[::-1], self.n_components]
)[::-1]
def rotate_cw(self) -> Texture:
"""Rotate this texture 90 degrees clockwise.
Returns
-------
pyvista.Texture
Rotated texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> rotated = texture.rotate_cw()
>>> rotated.plot()
"""
return Texture(np.rot90(self.to_array())) # type: ignore[abstract]
def rotate_ccw(self) -> Texture:
"""Rotate this texture 90 degrees counter-clockwise.
Returns
-------
pyvista.Texture
Rotated texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> rotated = texture.rotate_ccw()
>>> rotated.plot()
"""
return Texture(np.rot90(self.to_array(), k=3)) # type: ignore[abstract]
@property
def cube_map(self) -> bool: # numpydoc ignore=RT01
"""Return ``True`` if cube mapping is enabled and ``False`` otherwise."""
return self.GetCubeMap()
@cube_map.setter
def cube_map(self, flag: bool):
self.SetCubeMap(flag)
def copy(self): # type: ignore[override]
"""Make a copy of this texture.
Returns
-------
pyvista.Texture
Copied texture.
"""
return Texture(self.to_image().copy()) # type: ignore[abstract]
def to_skybox(self):
"""Return the texture as a :vtk:`vtkSkybox` if cube mapping is enabled.
Returns
-------
:vtk:`vtkSkybox`
Skybox if cube mapping is enabled. Otherwise, ``None``.
"""
if self.cube_map:
skybox = _vtk.vtkSkybox()
skybox.SetTexture(self)
return skybox
return None
def __repr__(self):
"""Return the object representation."""
return pyvista.DataSet.__repr__(self) # type: ignore[type-var]
def _get_attrs(self):
"""Return the representation methods (internal helper)."""
attrs = []
attrs.append(('Components', self.n_components, '{:d}'))
attrs.append(('Cube Map', self.cube_map, '{:}'))
attrs.append(('Dimensions', self.dimensions, '{:d}, {:d}')) # type: ignore[arg-type]
return attrs
@property
def n_components(self) -> int: # numpydoc ignore=RT01
"""Return the number of components in the image.
In textures, 3 or 4 components are used for representing RGB and RGBA
images.
Examples
--------
Show the number of components in the example masonry texture.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.n_components
3
"""
input_data = self.GetInput()
if input_data is None:
return 0
return input_data.GetPointData().GetScalars().GetNumberOfComponents()
@property
def dimensions(self) -> tuple[int, int]: # numpydoc ignore=RT01
"""Dimensions of the texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.dimensions
(256, 256)
"""
input_data = self.GetInput()
if input_data is None:
return (0, 0)
return input_data.GetDimensions()[:2]
def plot(self, **kwargs):
"""Plot the texture as an image.
If the texture is a cubemap, it will be displayed as a skybox with a
sphere in the center reflecting the environment.
Parameters
----------
**kwargs : dict, optional
Optional keyworld arguments. See :func:`pyvista.plot`.
Returns
-------
pyvista.Actor | None
See the returns section of :func:`pyvista.plot`.
Examples
--------
Plot a simple texture.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.plot()
Plot a cubemap as a skybox.
>>> cube_map = examples.download_sky_box_cube_map()
>>> cube_map.plot()
"""
if self.cube_map:
return self._plot_skybox(**kwargs)
kwargs.setdefault('zoom', 'tight')
kwargs.setdefault('lighting', False)
kwargs.setdefault('show_axes', False)
kwargs.setdefault('show_scalar_bar', False)
mesh = pyvista.Plane(i_size=self.dimensions[0], j_size=self.dimensions[1])
return mesh.plot(texture=self, **kwargs)
def _plot_skybox(self, **kwargs):
"""Plot this texture as a skybox."""
cpos = kwargs.pop('cpos', 'xy')
zoom = kwargs.pop('zoom', 0.5)
show_axes = kwargs.pop('show_axes', True)
lighting = kwargs.pop('lighting', None)
pl = pyvista.Plotter(lighting=lighting)
pl.add_actor(self.to_skybox())
pl.set_environment_texture(self, is_srgb=True)
pl.add_mesh(pyvista.Sphere(), pbr=True, roughness=0.5, metallic=1.0)
pl.camera_position = cpos
pl.camera.zoom(zoom)
if show_axes:
pl.show_axes()
pl.show(**kwargs)
@property
def wrap(self) -> Texture.WrapType: # numpydoc ignore=RT01
"""Return or set the Wrap mode for the texture coordinates.
Wrap mode for the texture coordinates valid values are:
* ``0`` - CLAMP_TO_EDGE
* ``1`` - REPEAT
* ``2`` - MIRRORED_REPEAT
* ``3`` - CLAMP_TO_BORDER
Notes
-----
CLAMP_TO_BORDER is not supported with OpenGL ES <= 3.2. Wrap will
default to CLAMP_TO_EDGE if it is set to CLAMP_TO_BORDER in this case.
Requires ``vtk`` v9.1.0 or newer.
Examples
--------
Load the masonry texture and create a simple :class:`pyvista.PolyData`
with texture coordinates using :func:`pyvista.Plane`. By default the
texture coordinates are between 0 and 1. Let's raise these values over
1 by multiplying them in place. This will allow us to wrap the texture.
>>> import pyvista as pv
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates *= 2
Let's now set the texture wrap to clamp to edge and visualize it.
>>> texture.wrap = pv.Texture.WrapType.CLAMP_TO_EDGE
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
Here is the default repeat:
>>> texture.wrap = pv.Texture.WrapType.REPEAT
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
And here is mirrored repeat:
>>> texture.wrap = pv.Texture.WrapType.MIRRORED_REPEAT
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
Finally, this is clamp to border:
>>> texture.wrap = pv.Texture.WrapType.CLAMP_TO_BORDER
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
if not hasattr(self, 'GetWrap'): # pragma: no cover
from pyvista.core.errors import VTKVersionError # noqa: PLC0415
msg = '`wrap` requires VTK v9.1.0 or newer.'
raise VTKVersionError(msg)
return Texture.WrapType(self.GetWrap()) # type: ignore[call-arg]
@wrap.setter
def wrap(self, value: Texture.WrapType | int):
if not hasattr(self, 'SetWrap'): # pragma: no cover
from pyvista.core.errors import VTKVersionError # noqa: PLC0415
msg = '`wrap` requires VTK v9.1.0 or newer.'
raise VTKVersionError(msg)
self.SetWrap(value)
def to_grayscale(self) -> Texture:
"""Convert this texture as a single component (grayscale) texture.
Returns
-------
pyvista.Texture
Texture converted to grayscale. If already grayscale, the original
texture itself is returned.
Notes
-----
The transparency channel (if available) will be dropped.
Follows the `CCIR 601 <https://en.wikipedia.org/wiki/Rec._601>`_ luma
calculation equation of ``Y = 0.299*R + 0.587*G + 0.114*B``.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> bw_texture = texture.to_grayscale()
>>> bw_texture
Texture (...)
Components: 1
Cube Map: False
Dimensions: 256, 256
>>> bw_texture.plot()
"""
if self.n_components == 1:
return self.copy()
data = self.to_array()
r, g, b = data[..., 0], data[..., 1], data[..., 2]
data = (0.299 * r + 0.587 * g + 0.114 * b).round().astype(np.uint8)
return Texture(data) # type: ignore[abstract]
def image_to_texture(image):
"""Convert :class:`pyvista.ImageData` to a :class:`pyvista.Texture`.
Parameters
----------
image : pyvista.ImageData | :vtk:`vtkImageData`
Image to convert.
Returns
-------
pyvista.Texture
The texture.
"""
return Texture(image) # type: ignore[abstract]
def numpy_to_texture(image):
"""Convert a NumPy image array to a :class:`pyvista.Texture`.
Parameters
----------
image : numpy.ndarray
Numpy image array. Texture datatype expected to be ``np.uint8``.
Returns
-------
pyvista.Texture
PyVista texture.
Examples
--------
Create an all white texture.
>>> import pyvista as pv
>>> import numpy as np
>>> tex_arr = np.ones((1024, 1024, 3), dtype=np.uint8) * 255
>>> tex = pv.numpy_to_texture(tex_arr)
"""
if image.dtype != np.uint8:
image = image.astype(np.uint8)
warnings.warn(
'Expected `image` dtype to be ``np.uint8``. `image` has been copied '
'and converted to np.uint8.',
UserWarning,
)
return Texture(image) # type: ignore[abstract]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,780 @@
"""Module containing useful plotting tools."""
from __future__ import annotations
from enum import Enum
import os
import platform
import subprocess
from subprocess import PIPE
from subprocess import Popen
from subprocess import TimeoutExpired
import sys
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk
from .colors import Color
class FONTS(Enum):
"""Font families available to PyVista."""
arial = _vtk.VTK_ARIAL
courier = _vtk.VTK_COURIER
times = _vtk.VTK_TIMES
# Track render window support and plotting
SUPPORTS_OPENGL = None
SUPPORTS_PLOTTING = None
def supports_open_gl():
"""Return if the system supports OpenGL.
This function checks if the system supports OpenGL by creating a VTK render
window and querying its OpenGL support.
Returns
-------
bool
``True`` if the system supports OpenGL, ``False`` otherwise.
"""
global SUPPORTS_OPENGL # noqa: PLW0603
if SUPPORTS_OPENGL is None:
ren_win = _vtk.vtkRenderWindow()
SUPPORTS_OPENGL = bool(ren_win.SupportsOpenGL())
return SUPPORTS_OPENGL
def _system_supports_plotting(): # noqa: PLR0911
"""Check if the environment supports plotting on Windows, Linux, or Mac OS.
Returns
-------
system_supports_plotting : bool
``True`` when system supports plotting.
"""
if os.environ.get('ALLOW_PLOTTING', '').lower() == 'true':
return True
# Windows case
if os.name == 'nt':
# actually have to check here. Somewhat expensive.
return supports_open_gl()
# mac case
if platform.system() == 'Darwin':
# check if finder available
proc = Popen(['pgrep', '-qx', 'Finder'], stdout=PIPE, stderr=PIPE, encoding='utf8')
try:
proc.communicate(timeout=10)
except TimeoutExpired:
return False
if proc.returncode == 0:
return True
# display variable set, likely available
return 'DISPLAY' in os.environ
# Linux case
try:
proc = Popen(['xset', '-q'], stdout=PIPE, stderr=PIPE, encoding='utf8')
proc.communicate(timeout=10)
except (OSError, TimeoutExpired):
return False
else: # pragma: no cover
return proc.returncode == 0
def system_supports_plotting():
"""Check if the environment supports plotting.
Returns
-------
bool
``True`` when system supports plotting.
"""
global SUPPORTS_PLOTTING # noqa: PLW0603
if SUPPORTS_PLOTTING is None:
SUPPORTS_PLOTTING = _system_supports_plotting()
# always use the cached response
return SUPPORTS_PLOTTING
def _update_axes_label_color(axes_actor, color=None):
"""Set the axes label color (internal helper)."""
color = Color(color, default_color=pyvista.global_theme.font.color)
if isinstance(axes_actor, _vtk.vtkAxesActor):
prop_x = axes_actor.GetXAxisCaptionActor2D().GetCaptionTextProperty()
prop_y = axes_actor.GetYAxisCaptionActor2D().GetCaptionTextProperty()
prop_z = axes_actor.GetZAxisCaptionActor2D().GetCaptionTextProperty()
for prop in [prop_x, prop_y, prop_z]:
prop.SetColor(color.float_rgb)
prop.SetShadow(False)
elif isinstance(axes_actor, _vtk.vtkAnnotatedCubeActor):
axes_actor.GetTextEdgesProperty().SetColor(color.float_rgb)
@_deprecate_positional_args
def create_axes_marker( # noqa: PLR0917
label_color=None,
x_color=None,
y_color=None,
z_color=None,
xlabel='X',
ylabel='Y',
zlabel='Z',
labels_off: bool = False, # noqa: FBT001, FBT002
line_width=2,
cone_radius=0.4,
shaft_length=0.8,
tip_length=0.2,
ambient=0.5,
label_size=(0.25, 0.1),
) -> _vtk.vtkAxesActor:
"""Create an axis actor.
Parameters
----------
label_color : ColorLike, optional
Color of the label text.
x_color : ColorLike, optional
Color of the x-axis text.
y_color : ColorLike, optional
Color of the y-axis text.
z_color : ColorLike, optional
Color of the z-axis text.
xlabel : str, default: "X"
Text used for the x-axis.
ylabel : str, default: "Y"
Text used for the y-axis.
zlabel : str, default: "Z"
Text used for the z-axis.
labels_off : bool, default: False
Enable or disable the text labels for the axes.
line_width : float, default: 2
The width of the marker lines.
cone_radius : float, default: 0.4
The radius of the axes arrow tips.
shaft_length : float, default: 0.8
The length of the axes arrow shafts.
tip_length : float, default: 0.2
Length of the tip.
ambient : float, default: 0.5
The ambient of the axes arrows. Value should be between 0 and 1.
label_size : sequence[float], default: (0.25, 0.1)
The width and height of the axes label actors. Values should be between
0 and 1. For example ``(0.2, 0.1)``.
Returns
-------
:vtk:`vtkAxesActor`
Axes actor.
Examples
--------
Create the default axes marker.
>>> import pyvista as pv
>>> marker = pv.create_axes_marker()
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(marker)
>>> pl.show()
Create an axes marker at the origin with custom colors and axis labels.
>>> import pyvista as pv
>>> marker = pv.create_axes_marker(
... line_width=4,
... ambient=0.0,
... x_color='#378df0',
... y_color='#ab2e5d',
... z_color='#f7fb9a',
... xlabel='X Axis',
... ylabel='Y Axis',
... zlabel='Z Axis',
... label_size=(0.1, 0.1),
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(marker)
>>> pl.show()
"""
x_color = Color(x_color, default_color=pyvista.global_theme.axes.x_color)
y_color = Color(y_color, default_color=pyvista.global_theme.axes.y_color)
z_color = Color(z_color, default_color=pyvista.global_theme.axes.z_color)
axes_actor = _vtk.vtkAxesActor()
axes_actor.GetXAxisShaftProperty().SetColor(x_color.float_rgb)
axes_actor.GetXAxisTipProperty().SetColor(x_color.float_rgb)
axes_actor.GetYAxisShaftProperty().SetColor(y_color.float_rgb)
axes_actor.GetYAxisTipProperty().SetColor(y_color.float_rgb)
axes_actor.GetZAxisShaftProperty().SetColor(z_color.float_rgb)
axes_actor.GetZAxisTipProperty().SetColor(z_color.float_rgb)
# Set labels
axes_actor.SetXAxisLabelText(xlabel)
axes_actor.SetYAxisLabelText(ylabel)
axes_actor.SetZAxisLabelText(zlabel)
if labels_off:
axes_actor.AxisLabelsOff()
# Set Line width
axes_actor.GetXAxisShaftProperty().SetLineWidth(line_width)
axes_actor.GetYAxisShaftProperty().SetLineWidth(line_width)
axes_actor.GetZAxisShaftProperty().SetLineWidth(line_width)
axes_actor.SetConeRadius(cone_radius)
axes_actor.SetNormalizedShaftLength([shaft_length] * 3)
axes_actor.SetNormalizedTipLength([tip_length] * 3)
axes_actor.GetXAxisShaftProperty().SetAmbient(ambient)
axes_actor.GetYAxisShaftProperty().SetAmbient(ambient)
axes_actor.GetZAxisShaftProperty().SetAmbient(ambient)
axes_actor.GetXAxisTipProperty().SetAmbient(ambient)
axes_actor.GetYAxisTipProperty().SetAmbient(ambient)
axes_actor.GetZAxisTipProperty().SetAmbient(ambient)
for label_actor in [
axes_actor.GetXAxisCaptionActor2D(),
axes_actor.GetYAxisCaptionActor2D(),
axes_actor.GetZAxisCaptionActor2D(),
]:
label_actor.SetWidth(label_size[0])
label_actor.SetHeight(label_size[1])
_update_axes_label_color(axes_actor, label_color)
return axes_actor
@_deprecate_positional_args
def create_axes_orientation_box( # noqa: PLR0917
line_width=1,
text_scale=0.366667,
edge_color='black',
x_color=None,
y_color=None,
z_color=None,
xlabel='X',
ylabel='Y',
zlabel='Z',
x_face_color='red',
y_face_color='green',
z_face_color='blue',
color_box: bool = False, # noqa: FBT001, FBT002
label_color=None,
labels_off: bool = False, # noqa: FBT001, FBT002
opacity=0.5,
show_text_edges: bool = False, # noqa: FBT001, FBT002
):
"""Create a Box axes orientation widget with labels.
Parameters
----------
line_width : float, optional
The width of the marker lines.
text_scale : float, optional
Size of the text relative to the faces.
edge_color : ColorLike, optional
Color of the edges.
x_color : ColorLike, optional
Color of the x-axis text.
y_color : ColorLike, optional
Color of the y-axis text.
z_color : ColorLike, optional
Color of the z-axis text.
xlabel : str, optional
Text used for the x-axis.
ylabel : str, optional
Text used for the y-axis.
zlabel : str, optional
Text used for the z-axis.
x_face_color : ColorLike, optional
Color used for the x-axis arrow. Defaults to theme axes
parameters.
y_face_color : ColorLike, optional
Color used for the y-axis arrow. Defaults to theme axes
parameters.
z_face_color : ColorLike, optional
Color used for the z-axis arrow. Defaults to theme axes
parameters.
color_box : bool, optional
Enable or disable the face colors. Otherwise, box is white.
label_color : ColorLike, optional
Color of the labels.
labels_off : bool, optional
Enable or disable the text labels for the axes.
opacity : float, optional
Opacity in the range of ``[0, 1]`` of the orientation box.
show_text_edges : bool, optional
Enable or disable drawing the vector text edges.
Returns
-------
:vtk:`vtkAnnotatedCubeActor`
Annotated cube actor.
Examples
--------
Create and plot an orientation box
>>> import pyvista as pv
>>> actor = pv.create_axes_orientation_box(
... line_width=1,
... text_scale=0.53,
... edge_color='black',
... x_color='k',
... y_color=None,
... z_color=None,
... xlabel='X',
... ylabel='Y',
... zlabel='Z',
... color_box=False,
... labels_off=False,
... opacity=1.0,
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(actor)
>>> pl.show()
"""
x_color = Color(x_color, default_color=pyvista.global_theme.axes.x_color)
y_color = Color(y_color, default_color=pyvista.global_theme.axes.y_color)
z_color = Color(z_color, default_color=pyvista.global_theme.axes.z_color)
edge_color = Color(edge_color, default_color=pyvista.global_theme.edge_color)
x_face_color = Color(x_face_color)
y_face_color = Color(y_face_color)
z_face_color = Color(z_face_color)
axes_actor = _vtk.vtkAnnotatedCubeActor()
axes_actor.SetFaceTextScale(text_scale)
if xlabel is not None:
axes_actor.SetXPlusFaceText(f'+{xlabel}')
axes_actor.SetXMinusFaceText(f'-{xlabel}')
if ylabel is not None:
axes_actor.SetYPlusFaceText(f'+{ylabel}')
axes_actor.SetYMinusFaceText(f'-{ylabel}')
if zlabel is not None:
axes_actor.SetZPlusFaceText(f'+{zlabel}')
axes_actor.SetZMinusFaceText(f'-{zlabel}')
axes_actor.SetFaceTextVisibility(not labels_off)
axes_actor.SetTextEdgesVisibility(show_text_edges)
# https://github.com/pyvista/pyvista/pull/5382
# axes_actor.GetTextEdgesProperty().SetColor(edge_color.float_rgb)
axes_actor.GetTextEdgesProperty().SetLineWidth(line_width)
axes_actor.GetXPlusFaceProperty().SetColor(x_color.float_rgb)
axes_actor.GetXMinusFaceProperty().SetColor(x_color.float_rgb)
axes_actor.GetYPlusFaceProperty().SetColor(y_color.float_rgb)
axes_actor.GetYMinusFaceProperty().SetColor(y_color.float_rgb)
axes_actor.GetZPlusFaceProperty().SetColor(z_color.float_rgb)
axes_actor.GetZMinusFaceProperty().SetColor(z_color.float_rgb)
axes_actor.GetCubeProperty().SetOpacity(opacity)
axes_actor.GetCubeProperty().SetEdgeColor(edge_color.float_rgb)
axes_actor.GetCubeProperty().SetEdgeVisibility(True)
axes_actor.GetCubeProperty().BackfaceCullingOn()
if opacity < 1.0:
# Hide the text edges
axes_actor.GetTextEdgesProperty().SetOpacity(0)
if color_box:
# Hide the cube so we can color each face
axes_actor.GetCubeProperty().SetOpacity(0)
axes_actor.GetCubeProperty().SetEdgeVisibility(False)
cube = pyvista.Cube()
cube.clear_data() # remove normals
face_colors = np.array(
[
x_face_color.int_rgb,
x_face_color.int_rgb,
y_face_color.int_rgb,
y_face_color.int_rgb,
z_face_color.int_rgb,
z_face_color.int_rgb,
],
np.uint8,
)
cube.cell_data['face_colors'] = face_colors
cube_mapper = _vtk.vtkPolyDataMapper()
cube_mapper.SetInputData(cube)
cube_mapper.SetColorModeToDirectScalars()
cube_mapper.Update()
cube_actor = pyvista.Actor(mapper=cube_mapper)
cube_actor.prop.culling = 'back'
cube_actor.prop.opacity = opacity
prop_assembly = _vtk.vtkPropAssembly()
prop_assembly.AddPart(axes_actor)
prop_assembly.AddPart(cube_actor)
actor = prop_assembly
else:
actor = axes_actor # type: ignore[assignment]
_update_axes_label_color(actor, label_color)
return actor
def create_north_arrow():
"""Create a north arrow mesh.
.. versionadded:: 0.44.0
Returns
-------
pyvista.PolyData
North arrow mesh.
"""
points = np.array(
[
[0.0, 5.0, 0.0],
[-2.0, 0.0, 0.0],
[0.0, 1.5, 0.0],
[2.0, 0.0, 0.0],
[0.0, 5.0, 1.0],
[-2.0, 0.0, 1.0],
[0.0, 1.5, 1.0],
[2.0, 0.0, 1.0],
],
)
faces = np.array(
[
4,
3,
7,
4,
0,
4,
2,
6,
7,
3,
4,
1,
5,
6,
2,
4,
0,
4,
5,
1,
4,
0,
1,
2,
3,
4,
4,
7,
6,
5,
],
)
return pyvista.PolyData(points, faces)
def normalize(x, minimum=None, maximum=None):
"""Normalize the given value between [minimum, maximum].
Parameters
----------
x : numpy.ndarray
The array of values to normalize.
minimum : float, optional
The minimum value to which ``x`` should be normalized. If not specified,
the minimum value in ``x`` will be used.
maximum : float, optional
The maximum value to which ``x`` should be normalized. If not specified,
the maximum value in ``x`` will be used.
Returns
-------
numpy.ndarray
The normalized array of values, where the values are scaled to the
range ``[minimum, maximum]``.
"""
if minimum is None:
minimum = np.nanmin(x)
if maximum is None:
maximum = np.nanmax(x)
return (x - minimum) / (maximum - minimum)
@_deprecate_positional_args(allowed=['mapping', 'n_colors'])
def opacity_transfer_function( # noqa: PLR0917
mapping,
n_colors,
interpolate: bool = True, # noqa: FBT001, FBT002
kind='linear',
):
"""Get the opacity transfer function for a mapping.
These values will map on to a scalar bar range and thus the number of
colors (``n_colors``) must correspond to the number of colors in the color
mapping that these opacities are associated to.
If interpolating, ``scipy.interpolate.interp1d`` is used if available,
otherwise ``np.interp`` is used. The ``kind`` argument controls the kind of
interpolation for ``interp1d``.
This returns the opacity range from 0 to 255, where 0 is totally
transparent and 255 is totally opaque.
The equation to create the sigmoid mapping is: ``1 / (1 + exp(-x))`` where
``x`` is the range from ``-a`` to ``+a`` and ``a`` is the value given in
the ``mapping`` string. Default is ``a=10`` for 'sigmoid' mapping.
Parameters
----------
mapping : list[float] | str
The opacity mapping to use. Can be a ``str`` name of a predefined
mapping including ``'linear'``, ``'geom'``, ``'sigmoid'``,
``'sigmoid_1-10,15,20'``, and ``foreground``. Append an ``'_r'`` to any
of those names (except ``foreground``) to reverse that mapping.
The mapping can also be a custom user-defined array/list of values
that will be interpolated across the ``n_color`` range.
n_colors : int
The number of colors that the opacities must be mapped to.
interpolate : bool
Flag on whether or not to interpolate the opacity mapping for all
colors.
kind : str
The interpolation kind if ``interpolate`` is ``True`` and ``scipy``
is available. If ``scipy`` is not available, linear interpolation
is always used. Options are:
- ``'linear'``
- ``'nearest'``
- ``'zero'``
- ``'slinear'``
- ``'quadratic'``
- ``'cubic'``
- ``'previous'``
- ``'next'``
.. versionchanged:: 0.46
Linear interpolation is now always used by default. Previously,
quadratic interpolation was used if ``scipy`` was installed.
Returns
-------
numpy.ndarray
Array of ``numpy.uint8`` values ``n_colors`` long containing the
[0-255] opacity mapping values.
Examples
--------
>>> import pyvista as pv
>>> # Fetch the `sigmoid` mapping between 0 and 255
>>> tf = pv.opacity_transfer_function('sigmoid', 256)
>>> # Fetch the `geom_r` mapping between 0 and 1
>>> tf = pv.opacity_transfer_function('geom_r', 256).astype(float) / 255.0
>>> # Interpolate a user defined opacity mapping
>>> opacity = [0, 0.2, 0.9, 0.6, 0.3]
>>> tf = pv.opacity_transfer_function(opacity, 256)
"""
sigmoid = lambda x: np.array(1 / (1 + np.exp(-x)) * 255, dtype=np.uint8)
transfer_func = {
'linear': np.linspace(0, 255, n_colors, dtype=np.uint8),
'geom': np.geomspace(1e-6, 255, n_colors, dtype=np.uint8),
'geom_r': np.geomspace(255, 1e-6, n_colors, dtype=np.uint8),
'sigmoid': sigmoid(np.linspace(-10.0, 10.0, n_colors)),
'sigmoid_1': sigmoid(np.linspace(-1.0, 1.0, n_colors)),
'sigmoid_2': sigmoid(np.linspace(-2.0, 2.0, n_colors)),
'sigmoid_3': sigmoid(np.linspace(-3.0, 3.0, n_colors)),
'sigmoid_4': sigmoid(np.linspace(-4.0, 4.0, n_colors)),
'sigmoid_5': sigmoid(np.linspace(-5.0, 5.0, n_colors)),
'sigmoid_6': sigmoid(np.linspace(-6.0, 6.0, n_colors)),
'sigmoid_7': sigmoid(np.linspace(-7.0, 7.0, n_colors)),
'sigmoid_8': sigmoid(np.linspace(-8.0, 8.0, n_colors)),
'sigmoid_9': sigmoid(np.linspace(-9.0, 9.0, n_colors)),
'sigmoid_10': sigmoid(np.linspace(-10.0, 10.0, n_colors)),
'sigmoid_15': sigmoid(np.linspace(-15.0, 15.0, n_colors)),
'sigmoid_20': sigmoid(np.linspace(-20.0, 20.0, n_colors)),
'foreground': np.hstack((0, [255] * (n_colors - 1))).astype(np.uint8),
}
transfer_func['linear_r'] = transfer_func['linear'][::-1]
transfer_func['sigmoid_r'] = transfer_func['sigmoid'][::-1]
for i in range(3, 11):
k = f'sigmoid_{i}'
rk = f'{k}_r'
transfer_func[rk] = transfer_func[k][::-1]
if isinstance(mapping, str):
try:
return transfer_func[mapping]
except KeyError:
msg = (
f'Opacity transfer function ({mapping}) unknown. '
f'Valid options: {list(transfer_func.keys())}'
)
raise ValueError(msg) from None
elif isinstance(mapping, (np.ndarray, list, tuple)):
mapping = np.array(mapping)
if mapping.size == n_colors:
# User could pass transfer function ready for lookup table
pass
elif mapping.size < n_colors:
# User pass custom transfer function to be linearly interpolated
if np.max(mapping) > 1.0 or np.min(mapping) < 0.0:
mapping = normalize(mapping)
# Interpolate transfer function to match lookup table
xo = np.linspace(0, n_colors, len(mapping), dtype=np.int_)
xx = np.linspace(0, n_colors, n_colors, dtype=np.int_)
try:
if not interpolate:
msg = 'No interpolation.'
raise ValueError(msg)
from scipy.interpolate import interp1d # noqa: PLC0415
f = interp1d(xo, mapping, kind=kind)
vals = f(xx)
vals[vals < 0] = 0.0
vals[vals > 1.0] = 1.0
mapping = (vals * 255.0).astype(np.uint8)
except (ImportError, ValueError):
# Otherwise use simple linear interp
mapping = (np.interp(xx, xo, mapping) * 255).astype(np.uint8)
else:
msg = (
f'Transfer function cannot have more values than `n_colors`. '
f'This has {mapping.size} elements'
)
raise RuntimeError(msg)
return mapping
msg = f'Transfer function type ({type(mapping)}) not understood'
raise TypeError(msg)
def parse_font_family(font_family: str) -> int:
"""Check and validate the given font family name.
Parameters
----------
font_family : str
Font family name to validate. Must be one of the font names defined in
the ``FONTS`` enum class.
Returns
-------
int
Corresponding integer value of the valid font family name in the
``FONTS`` enum class.
Raises
------
ValueError
If the font_family is not one of the defined font names in the ``FONTS``
enum class.
"""
font_family = font_family.lower()
fonts = [font.name for font in FONTS]
if font_family not in fonts:
msg = f'Font must one of the following:\n{", ".join(fonts)}'
raise ValueError(msg)
return FONTS[font_family].value
def check_matplotlib_vtk_compatibility():
"""Check if VTK and Matplotlib versions are compatible for MathText rendering.
This function is primarily geared towards checking if MathText rendering is
supported with the given versions of VTK and Matplotlib. It follows the
version constraints:
* VTK <= 9.2.2 requires Matplotlib < 3.6
* VTK > 9.2.2 requires Matplotlib >= 3.6
Other version combinations of VTK and Matplotlib will work without
errors, but some features (like MathText/LaTeX rendering) may
silently fail.
Returns
-------
bool
True if the versions of VTK and Matplotlib are compatible for MathText
rendering, False otherwise.
Raises
------
RuntimeError
If the versions of VTK and Matplotlib cannot be checked.
"""
import matplotlib as mpl # noqa: PLC0415
mpl_vers = tuple(map(int, mpl.__version__.split('.')[:2]))
if pyvista.vtk_version_info <= (9, 2, 2):
return not mpl_vers >= (3, 6)
elif pyvista.vtk_version_info > (9, 2, 2):
return mpl_vers >= (3, 6)
msg = 'Uncheckable versions.' # pragma: no cover
raise RuntimeError(msg) # pragma: no cover
def check_math_text_support():
"""Check if MathText and LaTeX symbols are supported.
Returns
-------
bool
``True`` if both MathText and LaTeX symbols are supported, ``False``
otherwise.
"""
# Something seriously sketchy is happening with this VTK code
# It seems to hijack stdout and stderr?
# See https://github.com/pyvista/pyvista/issues/4732
# This is a hack to get around that by executing the code in a subprocess
# and capturing the output:
# _vtk.vtkMathTextFreeTypeTextRenderer().MathTextIsSupported()
_cmd = 'import vtk;print(vtk.vtkMathTextFreeTypeTextRenderer().MathTextIsSupported());'
proc = subprocess.run([sys.executable, '-c', _cmd], check=False, capture_output=True)
math_text_support = False if proc.returncode else proc.stdout.decode().strip() == 'True'
return math_text_support and check_matplotlib_vtk_compatibility()
@@ -0,0 +1,28 @@
"""Plotting utilities."""
from __future__ import annotations
from .algorithms import active_scalars_algorithm as active_scalars_algorithm
from .algorithms import add_ids_algorithm as add_ids_algorithm
from .algorithms import algorithm_to_mesh_handler as algorithm_to_mesh_handler
from .algorithms import cell_data_to_point_data_algorithm as cell_data_to_point_data_algorithm
from .algorithms import crinkle_algorithm as crinkle_algorithm
from .algorithms import decimation_algorithm as decimation_algorithm
from .algorithms import extract_surface_algorithm as extract_surface_algorithm
from .algorithms import outline_algorithm as outline_algorithm
from .algorithms import point_data_to_cell_data_algorithm as point_data_to_cell_data_algorithm
from .algorithms import pointset_to_polydata_algorithm as pointset_to_polydata_algorithm
from .algorithms import set_algorithm_input as set_algorithm_input
from .algorithms import triangulate_algorithm as triangulate_algorithm
from .cubemap import cubemap as cubemap
from .cubemap import cubemap_from_filenames as cubemap_from_filenames
from .gl_checks import check_depth_peeling as check_depth_peeling
from .gl_checks import uses_egl as uses_egl
from .regression import compare_images as compare_images
from .regression import image_from_window as image_from_window
from .regression import remove_alpha as remove_alpha
from .regression import run_image_filter as run_image_filter
from .regression import wrap_image_array as wrap_image_array
from .sphinx_gallery import Scraper as Scraper
from .sphinx_gallery import _get_sg_image_scraper as _get_sg_image_scraper
from .xvfb import start_xvfb as start_xvfb
@@ -0,0 +1,633 @@
"""Internal :vtk:`vtkAlgorithm` support helpers."""
from __future__ import annotations
import traceback
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.errors import PyVistaPipelineError
from pyvista.core.utilities.helpers import wrap
from pyvista.core.utilities.misc import _NoNewAttrMixin
from pyvista.plotting import _vtk
if TYPE_CHECKING:
from pyvista.core.utilities.arrays import CellLiteral
from pyvista.core.utilities.arrays import PointLiteral
def algorithm_to_mesh_handler(
mesh_or_algo, port=0
) -> tuple[pyvista.DataSet, _vtk.vtkAlgorithm | _vtk.vtkAlgorithmOutput | None]:
"""Handle :vtk:`vtkAlgorithms` where mesh objects are expected.
This is a convenience method to handle :vtk:`vtkAlgorithms` when passed to methods
that expect a :class:`~pyvista.DataSet`. This method will check if the passed
object is a :vtk:`vtkAlgorithm` or :vtk:`vtkAlgorithmOutput` and if so,
return that algorithm's output dataset (mesh) as the mesh to be used by the
calling function.
Parameters
----------
mesh_or_algo : DataSet | :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput`
The input to be used as a data set (mesh) or :vtk:`vtkAlgorithm` object.
port : int, default: 0
If the input (``mesh_or_algo``) is an algorithm, this specifies which output
port to use on that algorithm for the returned mesh.
Returns
-------
mesh : pyvista.DataSet
The resulting mesh data set from the input.
algorithm : :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput` | None
If an algorithm is passed, it will be returned. Otherwise returns ``None``.
"""
if isinstance(mesh_or_algo, (_vtk.vtkAlgorithm, _vtk.vtkAlgorithmOutput)):
if isinstance(mesh_or_algo, _vtk.vtkAlgorithmOutput):
algo = mesh_or_algo.GetProducer()
# If vtkAlgorithmOutput, override port argument
port = mesh_or_algo.GetIndex()
output = mesh_or_algo
else:
algo = mesh_or_algo
output = algo.GetOutputPort(port)
algo.Update() # NOTE: this could be expensive... but we need it to get the mesh
# for legacy implementation. This can be refactored.
mesh = wrap(algo.GetOutputDataObject(port))
if mesh is None:
# This is known to happen with vtkPointSet and VTKPythonAlgorithmBase
# see workaround in PreserveTypeAlgorithmBase.
# This check remains as a fail-safe.
msg = 'The passed algorithm is failing to produce an output.' # type: ignore[unreachable]
raise PyVistaPipelineError(msg)
# NOTE: Return the vtkAlgorithmOutput only if port is non-zero. Segfaults can sometimes
# happen with vtkAlgorithmOutput. This logic will mostly avoid those issues.
# See https://gitlab.kitware.com/vtk/vtk/-/issues/18776
return mesh, output if port != 0 else algo
return mesh_or_algo, None
def set_algorithm_input(alg, inp, port=0):
"""Set the input to a :vtk:`vtkAlgorithm`.
Parameters
----------
alg : :vtk:`vtkAlgorithm`
The algorithm whose input is being set.
inp : :vtk:`vtkAlgorithm` | :vtk:`vtkAlgorithmOutput` | :vtk:`vtkDataObject`
The input to the algorithm.
port : int, default: 0
The input port.
"""
if isinstance(inp, _vtk.vtkAlgorithm):
alg.SetInputConnection(port, inp.GetOutputPort())
elif isinstance(inp, _vtk.vtkAlgorithmOutput):
alg.SetInputConnection(port, inp)
else:
alg.SetInputDataObject(port, inp)
class PreserveTypeAlgorithmBase(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase
):
"""Base algorithm to preserve type.
Parameters
----------
nInputPorts : int, default: 1
Number of input ports for the algorithm.
nOutputPorts : int, default: 1
Number of output ports for the algorithm.
"""
def __init__(self, nInputPorts=1, nOutputPorts=1):
"""Initialize algorithm."""
_vtk.VTKPythonAlgorithmBase.__init__(
self,
nInputPorts=nInputPorts,
nOutputPorts=nOutputPorts,
)
def GetInputData(self, inInfo, port, idx):
"""Get input data object.
This will convert :vtk:`vtkPointSet` to :vtk:`vtkPolyData`.
Parameters
----------
inInfo : :vtk:`vtkInformation`
The information object associated with the input port.
port : int
The index of the input port.
idx : int
The index of the data object within the input port.
Returns
-------
:vtk:`vtkDataObject`
The input data object.
"""
inp = wrap(_vtk.VTKPythonAlgorithmBase.GetInputData(self, inInfo, port, idx))
if isinstance(inp, pyvista.PointSet):
return inp.cast_to_polydata()
return inp
# THIS IS CRUCIAL to preserve data type through filter
def RequestDataObject(self, _request, inInfo, outInfo) -> int:
"""Preserve data type.
Parameters
----------
_request : :vtk:`vtkInformation`
The request object for the filter.
inInfo : :vtk:`vtkInformationVector`
The input information vector for the filter.
outInfo : :vtk:`vtkInformationVector`
The output information vector for the filter.
Returns
-------
int
Returns 1 if successful.
"""
class_name = self.GetInputData(inInfo, 0, 0).GetClassName()
if class_name == 'vtkPointSet':
# See https://gitlab.kitware.com/vtk/vtk/-/issues/18771
self.OutputType = 'vtkPolyData'
else:
self.OutputType = class_name
self.FillOutputPortInformation(0, outInfo.GetInformationObject(0))
return 1
class ActiveScalarsAlgorithm(PreserveTypeAlgorithmBase):
"""Algorithm to control active scalars.
The output of this filter is a shallow copy of the input data
set with the active scalars set as specified.
Parameters
----------
name : str
Name of scalars used to set as active on the output mesh.
Accepts a string name of an array that is present on the mesh.
Array should be sized as a single vector.
preference : str, default: 'point'
When ``mesh.n_points == mesh.n_cells`` and setting
scalars, this parameter sets how the scalars will be
mapped to the mesh. The default, ``'point'``, causes the
scalars to be associated with the mesh points. Can be
either ``'point'`` or ``'cell'``.
"""
def __init__(self, name: str, preference: PointLiteral | CellLiteral = 'point'):
"""Initialize algorithm."""
super().__init__()
self.scalars_name = name
self.preference = preference
def RequestData(self, _request, inInfo, outInfo) -> int:
"""Perform algorithm execution.
Parameters
----------
_request : :vtk:`vtkInformation`
The request object.
inInfo : :vtk:`vtkInformationVector`
Information about the input data.
outInfo : :vtk:`vtkInformationVector`
Information about the output data.
Returns
-------
int
1 on success.
"""
try:
inp = wrap(self.GetInputData(inInfo, 0, 0))
out = self.GetOutputData(outInfo, 0)
output = inp.copy()
if output.n_arrays:
output.set_active_scalars(self.scalars_name, preference=self.preference)
out.ShallowCopy(output)
except Exception: # pragma: no cover
traceback.print_exc()
raise
return 1
class PointSetToPolyDataAlgorithm(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase
):
"""Algorithm to cast PointSet to PolyData.
This is implemented with :func:`pyvista.PointSet.cast_to_polydata`.
"""
def __init__(self):
"""Initialize algorithm."""
_vtk.VTKPythonAlgorithmBase.__init__(
self,
nInputPorts=1,
nOutputPorts=1,
inputType='vtkPointSet',
outputType='vtkPolyData',
)
def RequestData(self, _request, inInfo, outInfo) -> int:
"""Perform algorithm execution.
Parameters
----------
_request : :vtk:`vtkInformation`
Information associated with the request.
inInfo : :vtk:`vtkInformationVector`
Information about the input data.
outInfo : :vtk:`vtkInformationVector`
Information about the output data.
Returns
-------
int
1 when successful.
"""
try:
inp = wrap(self.GetInputData(inInfo, 0, 0))
out = self.GetOutputData(outInfo, 0)
output = inp.cast_to_polydata(deep=False)
out.ShallowCopy(output)
except Exception: # pragma: no cover
traceback.print_exc()
raise
return 1
class AddIDsAlgorithm(PreserveTypeAlgorithmBase):
"""Algorithm to add point or cell IDs.
Output of this filter is a shallow copy of the input with
point and/or cell ID arrays added.
Parameters
----------
point_ids : bool, default: True
Whether to add point IDs.
cell_ids : bool, default: True
Whether to add cell IDs.
Raises
------
ValueError
If neither point IDs nor cell IDs are set.
"""
@_deprecate_positional_args
def __init__(self, point_ids: bool = True, cell_ids: bool = True): # noqa: FBT001, FBT002
"""Initialize algorithm."""
super().__init__()
if not point_ids and not cell_ids: # pragma: no cover
msg = 'IDs must be set for points or cells or both.'
raise ValueError(msg)
self.point_ids = point_ids
self.cell_ids = cell_ids
def RequestData(self, _request, inInfo, outInfo) -> int:
"""Perform algorithm execution.
Parameters
----------
_request : :vtk:`vtkInformation`
Information associated with the request.
inInfo : :vtk:`vtkInformationVector`
Information about the input data.
outInfo : :vtk:`vtkInformationVector`
Information about the output data.
Returns
-------
int
Returns 1 if the algorithm was successful.
Raises
------
Exception
If the algorithm fails to execute properly.
"""
try:
inp = wrap(self.GetInputData(inInfo, 0, 0))
out = self.GetOutputData(outInfo, 0)
output = inp.copy()
if self.point_ids:
output.point_data['point_ids'] = np.arange(0, output.n_points, dtype=int)
if self.cell_ids:
output.cell_data['cell_ids'] = np.arange(0, output.n_cells, dtype=int)
if output.active_scalars_name in ['point_ids', 'cell_ids']:
output.active_scalars_name = inp.active_scalars_name
out.ShallowCopy(output)
except Exception: # pragma: no cover
traceback.print_exc()
raise
return 1
class CrinkleAlgorithm(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.VTKPythonAlgorithmBase):
"""Algorithm to crinkle cell IDs."""
def __init__(self):
"""Initialize algorithm."""
super().__init__(
nInputPorts=2,
outputType='vtkUnstructuredGrid',
)
def RequestData(self, _request, inInfo, outInfo) -> int:
"""Perform algorithm execution based on the input data and produce the output.
Parameters
----------
_request : :vtk:`vtkInformation`
The request information associated with the algorithm.
inInfo : :vtk:`vtkInformationVector`
Information vector describing the input data.
outInfo : :vtk:`vtkInformationVector`
Information vector where the output data should be placed.
Returns
-------
int
Status of the execution. Returns 1 on successful completion.
"""
try:
clipped = wrap(self.GetInputData(inInfo, 0, 0))
source = wrap(self.GetInputData(inInfo, 1, 0))
out = self.GetOutputData(outInfo, 0)
output = source.extract_cells(np.unique(clipped.cell_data['cell_ids']))
out.ShallowCopy(output)
except Exception: # pragma: no cover
traceback.print_exc()
raise
return 1
@_deprecate_positional_args(allowed=['inp'])
def outline_algorithm(inp, generate_faces: bool = False): # noqa: FBT001, FBT002
"""Add :vtk:`vtkOutlineFilter` to pipeline.
Parameters
----------
inp : pyvista.Common
Input data to be filtered.
generate_faces : bool, default: False
Whether to generate faces for the outline.
Returns
-------
:vtk:`vtkOutlineFilter`
Outline filter applied to the input data.
"""
alg = _vtk.vtkOutlineFilter()
set_algorithm_input(alg, inp)
alg.SetGenerateFaces(generate_faces)
return alg
@_deprecate_positional_args(allowed=['inp'])
def extract_surface_algorithm( # noqa: PLR0917
inp,
pass_pointid: bool = False, # noqa: FBT001, FBT002
pass_cellid: bool = False, # noqa: FBT001, FBT002
nonlinear_subdivision=1,
):
"""Add :vtk:`vtkDataSetSurfaceFilter` to pipeline.
Parameters
----------
inp : pyvista.Common
Input data to be filtered.
pass_pointid : bool, default: False
If ``True``, pass point IDs to the output.
pass_cellid : bool, default: False
If ``True``, pass cell IDs to the output.
nonlinear_subdivision : int, default: 1
Level of nonlinear subdivision.
Returns
-------
:vtk:`vtkDataSetSurfaceFilter`
Surface filter applied to the input data.
"""
surf_filter = _vtk.vtkDataSetSurfaceFilter()
surf_filter.SetPassThroughPointIds(pass_pointid)
surf_filter.SetPassThroughCellIds(pass_cellid)
if nonlinear_subdivision != 1:
surf_filter.SetNonlinearSubdivisionLevel(nonlinear_subdivision)
set_algorithm_input(surf_filter, inp)
return surf_filter
def active_scalars_algorithm(inp, name, preference='point'):
"""Add a filter that sets the active scalars.
Parameters
----------
inp : pyvista.Common
Input data to be filtered.
name : str
Name of the scalars to set as active.
preference : str, default: 'point'
Preference for the scalars to be set as active. Options are 'point', 'cell', or 'field'.
Returns
-------
:vtk:`vtkAlgorithm`
Active scalars filter applied to the input data.
"""
alg = ActiveScalarsAlgorithm(
name=name,
preference=preference,
)
set_algorithm_input(alg, inp)
return alg
def pointset_to_polydata_algorithm(inp):
"""Add a filter that casts PointSet to PolyData.
Parameters
----------
inp : pyvista.PointSet
Input point set to be cast to PolyData.
Returns
-------
:vtk:`vtkAlgorithm`
Filter that casts the input PointSet to PolyData.
"""
alg = PointSetToPolyDataAlgorithm()
set_algorithm_input(alg, inp)
return alg
@_deprecate_positional_args(allowed=['inp'])
def add_ids_algorithm(inp, point_ids: bool = True, cell_ids: bool = True): # noqa: FBT001, FBT002
"""Add a filter that adds point and/or cell IDs.
Parameters
----------
inp : pyvista.DataSet
The input data to which the IDs will be added.
point_ids : bool, default: True
If ``True``, point IDs will be added to the input data.
cell_ids : bool, default: True
If ``True``, cell IDs will be added to the input data.
Returns
-------
AddIDsAlgorithm
AddIDsAlgorithm filter.
"""
alg = AddIDsAlgorithm(point_ids=point_ids, cell_ids=cell_ids)
set_algorithm_input(alg, inp)
return alg
def crinkle_algorithm(clip, source):
"""Add a filter that crinkles a clip.
Parameters
----------
clip : pyvista.DataSet
The input data to be crinkled.
source : pyvista.DataSet
The source of the crinkle.
Returns
-------
CrinkleAlgorithm
CrinkleAlgorithm filter.
"""
alg = CrinkleAlgorithm()
set_algorithm_input(alg, clip, 0)
set_algorithm_input(alg, source, 1)
return alg
@_deprecate_positional_args(allowed=['inp'])
def cell_data_to_point_data_algorithm(inp, pass_cell_data: bool = False): # noqa: FBT001, FBT002
"""Add a filter that converts cell data to point data.
Parameters
----------
inp : pyvista.DataSet
The input data whose cell data will be converted to point data.
pass_cell_data : bool, default: False
If ``True``, the original cell data will be passed to the output.
Returns
-------
:vtk:`vtkCellDataToPointData`
The :vtk:`vtkCellDataToPointData` filter.
"""
alg = _vtk.vtkCellDataToPointData()
alg.SetPassCellData(pass_cell_data)
set_algorithm_input(alg, inp)
return alg
@_deprecate_positional_args(allowed=['inp'])
def point_data_to_cell_data_algorithm(inp, pass_point_data: bool = False): # noqa: FBT001, FBT002
"""Add a filter that converts point data to cell data.
Parameters
----------
inp : pyvista.DataSet
The input data whose point data will be converted to cell data.
pass_point_data : bool, default: False
If ``True``, the original point data will be passed to the output.
Returns
-------
:vtk:`vtkPointDataToCellData`
:vtk:`vtkPointDataToCellData` algorithm.
"""
alg = _vtk.vtkPointDataToCellData()
alg.SetPassPointData(pass_point_data)
set_algorithm_input(alg, inp)
return alg
def triangulate_algorithm(inp):
"""Triangulate the input data.
Parameters
----------
inp : :vtk:`vtkDataObject`
The input data to be triangulated.
Returns
-------
:vtk:`vtkTriangleFilter`
The triangle filter that has been applied to the input data.
"""
trifilter = _vtk.vtkTriangleFilter()
trifilter.PassVertsOff()
trifilter.PassLinesOff()
set_algorithm_input(trifilter, inp)
return trifilter
def decimation_algorithm(inp, target_reduction):
"""Decimate the input data to the target reduction.
Parameters
----------
inp : :vtk:`vtkDataObject`
The input data to be decimated.
target_reduction : float
The target reduction amount, as a fraction of the original data.
Returns
-------
:vtk:`vtkQuadricDecimation`
The decimation algorithm that has been applied to the input data.
"""
alg = _vtk.vtkQuadricDecimation()
alg.SetTargetReduction(target_reduction)
set_algorithm_input(alg, inp)
return alg
@@ -0,0 +1,131 @@
"""Cubemap utilities."""
from __future__ import annotations
from pathlib import Path
import pyvista
def cubemap(path='', prefix='', ext='.jpg'):
"""Construct a cubemap from 6 images from a directory.
Each of the 6 images must be in the following format:
- <prefix>negx<ext>
- <prefix>negy<ext>
- <prefix>negz<ext>
- <prefix>posx<ext>
- <prefix>posy<ext>
- <prefix>posz<ext>
Prefix may be empty, and extension will default to ``'.jpg'``
For example, if you have 6 images with the skybox2 prefix:
- ``'skybox2-negx.jpg'``
- ``'skybox2-negy.jpg'``
- ``'skybox2-negz.jpg'``
- ``'skybox2-posx.jpg'``
- ``'skybox2-posy.jpg'``
- ``'skybox2-posz.jpg'``
Parameters
----------
path : str, default: ""
Directory containing the cubemap images.
prefix : str, default: ""
Prefix to the filename.
ext : str, default: ".jpg"
The filename extension. For example ``'.jpg'``.
Returns
-------
pyvista.Texture
Texture with cubemap.
Notes
-----
Cubemap will appear flipped relative to the XY plane between VTK v9.1 and
VTK v9.2.
Examples
--------
Load a skybox given a directory, prefix, and file extension.
>>> import pyvista as pv
>>> skybox = pv.cubemap('my_directory', 'skybox', '.jpeg') # doctest:+SKIP
"""
sets = ['posx', 'negx', 'posy', 'negy', 'posz', 'negz']
image_paths = [str(Path(path) / f'{prefix}{suffix}{ext}') for suffix in sets]
return _cubemap_from_paths(image_paths)
def cubemap_from_filenames(image_paths):
"""Construct a cubemap from 6 images.
Images must be in the following order:
- Positive X
- Negative X
- Positive Y
- Negative Y
- Positive Z
- Negative Z
Parameters
----------
image_paths : sequence[str]
Paths of the individual cubemap images.
Returns
-------
pyvista.Texture
Texture with cubemap.
Examples
--------
Load a skybox given a list of image paths.
>>> image_paths = [
... '/home/user/_px.jpg',
... '/home/user/_nx.jpg',
... '/home/user/_py.jpg',
... '/home/user/_ny.jpg',
... '/home/user/_pz.jpg',
... '/home/user/_nz.jpg',
... ]
>>> skybox = pv.cubemap(image_paths=image_paths) # doctest:+SKIP
"""
if len(image_paths) != 6:
msg = 'image_paths must contain 6 paths'
raise ValueError(msg)
return _cubemap_from_paths(image_paths)
def _cubemap_from_paths(image_paths):
"""Construct a cubemap from image paths."""
for image_path in image_paths:
if not Path(image_path).is_file():
file_str = '\n'.join(image_paths)
msg = (
f'Unable to locate {image_path}\nExpected to find the following files:\n{file_str}'
)
raise FileNotFoundError(msg)
texture = pyvista.Texture() # type: ignore[abstract]
texture.SetMipmap(True)
texture.SetInterpolate(True)
texture.cube_map = True # Must be set prior to setting images
# add each image to the cubemap
for i, fn in enumerate(image_paths):
# Read and flip along y-axis
texture.SetInputDataObject(i, pyvista.read(fn)._flip_uniform(1))
return texture
@@ -0,0 +1,64 @@
"""Plotting GL checks."""
from __future__ import annotations
from pyvista.plotting import _vtk
def check_depth_peeling(number_of_peels=100, occlusion_ratio=0.0):
"""Check if depth peeling is available.
Attempts to use depth peeling to see if it is available for the
current environment. Returns ``True`` if depth peeling is
available and has been successfully leveraged, otherwise
``False``.
Parameters
----------
number_of_peels : int, default: 100
Maximum number of depth peels.
occlusion_ratio : float, default: 0.0
Occlusion ratio.
Returns
-------
bool
``True`` when system supports depth peeling with the specified
settings.
"""
# Try Depth Peeling with a basic scene
source = _vtk.vtkSphereSource()
mapper = _vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
actor = _vtk.vtkActor()
actor.SetMapper(mapper)
# requires opacity < 1
actor.GetProperty().SetOpacity(0.5)
renderer = _vtk.vtkRenderer()
renderWindow = _vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
renderWindow.SetOffScreenRendering(True)
renderWindow.SetAlphaBitPlanes(True)
renderWindow.SetMultiSamples(0)
renderer.AddActor(actor)
renderer.SetUseDepthPeeling(True)
renderer.SetMaximumNumberOfPeels(number_of_peels)
renderer.SetOcclusionRatio(occlusion_ratio)
renderWindow.Render()
return renderer.GetLastRenderingUsedDepthPeeling() == 1
def uses_egl() -> bool:
"""Check if VTK has been compiled with EGL support via OSMesa.
Returns
-------
bool
``True`` if VTK has been compiled with EGL support via OSMesa,
otherwise ``False``.
"""
ren_win_str = str(type(_vtk.vtkRenderWindow()))
return 'EGL' in ren_win_str or 'OSOpenGL' in ren_win_str
@@ -0,0 +1,272 @@
"""Image regression module."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import cast
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.arrays import point_array
from pyvista.core.utilities.helpers import wrap
from pyvista.plotting import _vtk
if TYPE_CHECKING:
from pyvista import ImageData
from pyvista.core._typing_core import NumpyArray
def remove_alpha(img):
"""Remove the alpha channel from a :vtk:`vtkImageData`.
Parameters
----------
img : :vtk:`vtkImageData`
The input image data with an alpha channel.
Returns
-------
ImageData
The output image data with the alpha channel removed.
"""
ec = _vtk.vtkImageExtractComponents()
ec.SetComponents(0, 1, 2)
ec.SetInputData(img)
ec.Update()
return pyvista.wrap(ec.GetOutput())
def wrap_image_array(arr):
"""Wrap a numpy array as a pyvista.ImageData.
Parameters
----------
arr : np.ndarray
A numpy array of shape (X, Y, (3 or 4)) and dtype ``np.uint8``. For
example, an array of shape ``(768, 1024, 3)``.
Raises
------
ValueError
If the input array does not have 3 dimensions, the third dimension of
the input array is not 3 or 4, or the input array is not of type
``np.uint8``.
Returns
-------
pyvista.ImageData
A PyVista ImageData object with the wrapped array data.
"""
if arr.ndim != 3:
msg = 'Expecting a X by Y by (3 or 4) array'
raise ValueError(msg)
if arr.shape[2] not in [3, 4]:
msg = 'Expecting a X by Y by (3 or 4) array'
raise ValueError(msg)
if arr.dtype != np.uint8:
msg = 'Expecting a np.uint8 array'
raise ValueError(msg)
img = _vtk.vtkImageData()
img.SetDimensions(arr.shape[1], arr.shape[0], 1)
wrap_img = pyvista.wrap(img)
wrap_img.point_data['PNGImage'] = arr[::-1].reshape(-1, arr.shape[2])
return wrap_img
def run_image_filter(imfilter: _vtk.vtkWindowToImageFilter) -> NumpyArray[float]:
"""Run a :vtk:`vtkWindowToImageFilter` and get output as array.
Parameters
----------
imfilter : :vtk:`vtkWindowToImageFilter`
The :vtk:`vtkWindowToImageFilter` instance to be processed.
Notes
-----
An empty array will be returned if an image cannot be extracted.
Returns
-------
numpy.ndarray
An array containing the filtered image data. The shape of the array
is given by (height, width, -1) where height and width are the
dimensions of the image.
"""
# Update filter and grab pixels
imfilter.Modified()
imfilter.Update()
image = cast('ImageData | None', wrap(imfilter.GetOutput()))
if image is None:
return np.empty((0, 0, 0))
img_size = image.dimensions
img_array = cast('NumpyArray[float]', point_array(image, 'ImageScalars'))
# Reshape and write
tgt_size = (img_size[1], img_size[0], -1)
return img_array.reshape(tgt_size)[::-1]
@_deprecate_positional_args(allowed=['render_window'])
def image_from_window( # noqa: PLR0917
render_window,
as_vtk: bool = False, # noqa: FBT001, FBT002
ignore_alpha: bool = False, # noqa: FBT001, FBT002
scale=1,
):
"""Extract the image from the render window as an array.
Parameters
----------
render_window : :vtk:`vtkRenderWindow`
The render window to extract the image from.
as_vtk : bool, default: False
If set to True, the image will be returned as a VTK object.
ignore_alpha : bool, default: False
If set to True, the image will be returned in RGB format,
otherwise, it will be returned in RGBA format.
scale : int, default: 1
The scaling factor of the extracted image. The default value is 1
which means that no scaling is applied.
Returns
-------
ndarray | :vtk:`vtkImageData`
The image as an array or as a VTK object depending on the ``as_vtk`` parameter.
"""
off = not render_window.GetInteractor().GetEnableRender()
if off:
render_window.GetInteractor().EnableRenderOn()
imfilter = _vtk.vtkWindowToImageFilter()
imfilter.SetInput(render_window)
imfilter.SetScale(scale)
imfilter.FixBoundaryOn()
imfilter.ReadFrontBufferOff()
imfilter.ShouldRerenderOff()
if ignore_alpha:
imfilter.SetInputBufferTypeToRGB()
else:
imfilter.SetInputBufferTypeToRGBA()
imfilter.ReadFrontBufferOn()
data = run_image_filter(imfilter)
if off:
# Critical for Trame and other offscreen tools
render_window.GetInteractor().EnableRenderOff()
if as_vtk:
return wrap_image_array(data)
return data
@_deprecate_positional_args(allowed=['im1', 'im2'])
def compare_images( # noqa: PLR0917
im1,
im2,
threshold=1,
use_vtk: bool = True, # noqa: FBT001, FBT002
):
"""Compare two different images of the same size.
Parameters
----------
im1 : str | numpy.ndarray | :vtk:`vtkRenderWindow` | :vtk:`vtkImageData`
Render window, numpy array representing the output of a render
window, or :vtk:`vtkImageData`.
im2 : str | numpy.ndarray | :vtk:`vtkRenderWindow` | :vtk:`vtkImageData`
Render window, numpy array representing the output of a render
window, or :vtk:`vtkImageData`.
threshold : int, default: 1
Threshold tolerance for pixel differences. This should be
greater than 0, otherwise it will always return an error, even
on identical images.
use_vtk : bool, default: True
When disabled, computes the mean pixel error over the entire
image using numpy. The difference between pixel is calculated
for each RGB channel, summed, and then divided by the number
of pixels. This is faster than using
:vtk:`vtkImageDifference` but potentially less accurate.
Returns
-------
float
Total error between the images if using ``use_vtk=True``, and
the mean pixel error when ``use_vtk=False``.
Examples
--------
Compare two active plotters.
>>> import pyvista as pv
>>> pl1 = pv.Plotter()
>>> _ = pl1.add_mesh(pv.Sphere(), smooth_shading=True)
>>> pl2 = pv.Plotter()
>>> _ = pl2.add_mesh(pv.Sphere(), smooth_shading=False)
>>> error = pv.compare_images(pl1, pl2)
Compare images from file.
>>> import pyvista as pv
>>> img1 = pv.read('img1.png') # doctest:+SKIP
>>> img2 = pv.read('img2.png') # doctest:+SKIP
>>> pv.compare_images(img1, img2) # doctest:+SKIP
"""
from pyvista import ImageData # noqa: PLC0415
from pyvista import Plotter # noqa: PLC0415
from pyvista import read # noqa: PLC0415
from pyvista import wrap # noqa: PLC0415
def to_img(img):
if isinstance(img, ImageData): # pragma: no cover
return img
elif isinstance(img, _vtk.vtkImageData):
return wrap(img)
elif isinstance(img, str):
return read(img)
elif isinstance(img, np.ndarray):
return wrap_image_array(img)
elif isinstance(img, Plotter):
if img._first_time: # must be rendered first else segfault
img._on_first_render_request()
img.render()
if img.render_window is None:
msg = 'Unable to extract image from Plotter as it has already been closed.'
raise RuntimeError(msg)
return image_from_window(img.render_window, as_vtk=True, ignore_alpha=True)
else:
msg = (
f'Unsupported data type {type(img)}. Should be '
'Either a np.ndarray, vtkRenderWindow, or vtkImageData'
)
raise TypeError(msg)
im1 = remove_alpha(to_img(im1))
im2 = remove_alpha(to_img(im2))
if im1.GetDimensions() != im2.GetDimensions():
msg = 'Input images are not the same size.'
raise RuntimeError(msg)
if use_vtk:
img_diff = _vtk.vtkImageDifference()
img_diff.SetThreshold(threshold)
img_diff.SetInputData(im1)
img_diff.SetImageData(im2)
img_diff.AllowShiftOff() # vastly increases compute time when enabled
# img_diff.AveragingOff() # increases compute time
img_diff.Update()
return img_diff.GetThresholdedError()
# otherwise, simply compute the mean pixel difference
diff = np.abs(im1.point_data[0] - im2.point_data[0])
return np.sum(diff) / im1.point_data[0].shape[0]
@@ -0,0 +1,229 @@
"""Utilities for using pyvista with sphinx-gallery."""
from __future__ import annotations
from pathlib import Path
import shutil
from typing import TYPE_CHECKING
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
if TYPE_CHECKING:
from collections.abc import Iterator
BUILDING_GALLERY_ERROR_MSG = (
'pyvista.BUILDING_GALLERY must be set to True in your conf.py to capture '
'images within sphinx_gallery or when building documentation using the '
'pyvista-plot directive.'
)
def _get_sg_image_scraper():
"""Return the callable scraper to be used by Sphinx-Gallery.
It allows PyVista users to just use strings as they already can for
'matplotlib' and 'mayavi'. Details on this implementation can be found in
`sphinx-gallery/sphinx-gallery/494`_
This must be imported into the top level namespace of PyVista.
.. _sphinx-gallery/sphinx-gallery/494: https://github.com/sphinx-gallery/sphinx-gallery/pull/494
"""
return Scraper()
def html_rst(
figure_list,
sources_dir,
srcsetpaths=None,
): # pragma: no cover # numpydoc ignore=PR01,RT01
"""Generate reST for viewer with exported scene."""
from sphinx_gallery.scrapers import _get_srcset_st # noqa: PLC0415
from sphinx_gallery.scrapers import figure_rst # noqa: PLC0415
if srcsetpaths is None:
# this should never happen, but figure_rst is public, so
# this has to be a kwarg...
srcsetpaths = [{0: fl} for fl in figure_list]
images_rst = ''
for i, hinnames in enumerate(srcsetpaths):
srcset = _get_srcset_st(sources_dir, hinnames)
if srcset[-5:] == 'vtksz':
png_file = figure_list[i][:-5] + 'png'
indented_firgure_rst = '\n'.join(
' ' * 5 + line for line in figure_rst([png_file], sources_dir).split('\n')
)
images_rst += f"""
\n
\n
.. tab-set::\n
\n
.. tab-item:: Static Scene\n
\n
{indented_firgure_rst}
\n
.. tab-item:: Interactive Scene\n
\n
.. offlineviewer:: {figure_list[i]}\n\n"""
else:
images_rst += '\n' + figure_rst([figure_list[i]], sources_dir) + '\n\n'
return images_rst
def _process_events_before_scraping(plotter):
"""Process events such as changing the camera or an object before scraping."""
if plotter.iren is not None and plotter.iren.initialized:
# check for pyvistaqt app which can be specifically bound to pyvista plotter
# objects in order to interact with qt, then process the events from qt
if hasattr(plotter, 'app') and plotter.app is not None:
plotter.app.processEvents()
plotter.update()
@_deprecate_positional_args(allowed=['image_path_iterator'])
def generate_images(image_path_iterator: Iterator[str], dynamic: bool = False) -> list[str]: # noqa: FBT001, FBT002
"""Generate images from the current plotters.
The file names are taken from the ``image_path_iterator`` iterator.
A gif will be created if a plotter has a ``_gif_filename`` attribute.
Otherwise, depending on the value of ``dynamic``, either a ``.png`` static image
or a ``.vtksz`` file will be created.
Parameters
----------
image_path_iterator : Iterator[str]
An iterator that yields the path to the next image to be saved.
dynamic : bool, default: False
Whether to save a static ``.png`` image or a ``.vtksz`` (interactive)
file.
Returns
-------
list[str]
A list of the names of the images that were created.
"""
image_names = []
figures = pyvista.plotting.plotter._ALL_PLOTTERS
for plotter in figures.values():
_process_events_before_scraping(plotter)
fname = next(image_path_iterator)
# Make sure the extension is "png"
path = Path(fname)
fname_withoutextension = str(path.parent / path.stem)
fname = fname_withoutextension + '.png'
if (gif_filename := plotter._gif_filename) is not None:
# move gif to fname
fname = fname[:-3] + 'gif'
shutil.move(gif_filename, fname)
image_names.append(fname)
else:
plotter.screenshot(fname)
if not dynamic or plotter.last_vtksz is None:
image_names.append(fname)
else: # pragma: no cover
fname = fname[:-3] + 'vtksz'
with Path(fname).open('wb') as f:
f.write(plotter.last_vtksz) # type: ignore[arg-type]
image_names.append(fname)
pyvista.close_all() # close and clear all plotters
return image_names
class Scraper:
"""Save ``pyvista.Plotter`` objects.
Used by sphinx-gallery to generate the plots from the code in the examples.
Pass an instance of this class to ``sphinx_gallery_conf`` in your
``conf.py`` as the ``"image_scrapers"`` argument.
Be sure to set ``pyvista.BUILDING_GALLERY = True`` in your ``conf.py``.
"""
def __repr__(self) -> str:
"""Return a stable representation of the class instance."""
return f'<{type(self).__name__} object>'
def __call__(self, block, block_vars, gallery_conf): # noqa: ARG002
"""Save the figures generated after running example code.
Called by sphinx-gallery.
"""
from sphinx_gallery.scrapers import figure_rst # noqa: PLC0415
if not pyvista.BUILDING_GALLERY:
raise RuntimeError(BUILDING_GALLERY_ERROR_MSG)
image_path_iterator = block_vars['image_path_iterator']
image_names = generate_images(image_path_iterator, dynamic=False)
return figure_rst(image_names, gallery_conf['src_dir'])
class DynamicScraper: # pragma: no cover
"""Save ``pyvista.Plotter`` objects dynamically.
Used by sphinx-gallery to generate the plots from the code in the examples.
Pass an instance of this class to ``sphinx_gallery_conf`` in your
``conf.py`` as the ``"image_scrapers"`` argument.
Be sure to set ``pyvista.BUILDING_GALLERY = True`` in your ``conf.py``.
If the boolean variable ``PYVISTA_GALLERY_FORCE_STATIC_IN_DOCUMENT = True/False``
is set as a global variable in the document then its value will be used as default for the
force_static argument of the pyvista-plot command. see also the notes at :func:plot_directive
To alter the global value behavior just for some plots you may set the
boolean variable ``PYVISTA_GALLERY_FORCE_STATIC = True``/
``PYVISTA_GALLERY_FORCE_STATIC = False`` just before the appropriate ``plot`` command.
The default behavior of this scraper is to create interactive plots.
"""
def __repr__(self) -> str:
"""Return a stable representation of the class instance."""
return f'<{type(self).__name__} object>'
def __call__(self, block, block_vars, gallery_conf): # pragma: no cover
"""Save the figures generated after running example code.
Called by sphinx-gallery.
"""
if not pyvista.BUILDING_GALLERY:
raise RuntimeError(BUILDING_GALLERY_ERROR_MSG)
# read global option if it exists
force_static = block_vars['example_globals'].get(
'PYVISTA_GALLERY_FORCE_STATIC_IN_DOCUMENT',
False,
)
# override with block specific value if it exists
if 'PYVISTA_GALLERY_FORCE_STATIC = True' in block[1].split('\n'):
force_static = True
elif 'PYVISTA_GALLERY_FORCE_STATIC = False' in block[1].split('\n'):
force_static = False
if force_static is None:
# Just in case force_static is None at this point
force_static = False
dynamic = not force_static
image_path_iterator = block_vars['image_path_iterator']
image_names = generate_images(image_path_iterator, dynamic=dynamic)
return html_rst(image_names, gallery_conf['src_dir'])
@@ -0,0 +1,71 @@
"""Start xvfb from Python."""
from __future__ import annotations
import os
import time
import warnings
from pyvista.core.errors import PyVistaDeprecationWarning
XVFB_INSTALL_NOTES = """Please install Xvfb with:
Debian
$ sudo apt install libgl1-mesa-glx xvfb
CentOS / RHL
$ sudo yum install libgl1-mesa-glx xvfb
"""
def start_xvfb(wait=3, window_size=None):
"""Start the virtual framebuffer Xvfb.
Parameters
----------
wait : float, optional
Time to wait for the virtual framebuffer to start. Set to 0
to disable wait.
window_size : list, optional
Window size of the virtual frame buffer. Defaults to
:attr:`pyvista.global_theme.window_size
<pyvista.plotting.themes.Theme.window_size>`.
Notes
-----
Only available on Linux. Be sure to install ``xvfb``
and ``libgl1-mesa-glx`` in your package manager.
Examples
--------
>>> import pyvista as pv
>>> pv.start_xvfb() # doctest:+SKIP
"""
# Deprecated on 0.45.0, estimated removal on 0.48.0
warnings.warn(
'This function is deprecated and will be removed in future version of '
'PyVista. Use vtk-osmesa instead.',
PyVistaDeprecationWarning,
)
from pyvista import global_theme # noqa: PLC0415
if os.name != 'posix':
msg = '`start_xvfb` is only supported on Linux'
raise OSError(msg)
if os.system('which Xvfb > /dev/null'):
raise OSError(XVFB_INSTALL_NOTES)
# use current default window size
if window_size is None:
window_size = global_theme.window_size
window_size_parm = f'{window_size[0]:d}x{window_size[1]:d}x24'
display_num = ':99'
os.system(f'Xvfb {display_num} -screen 0 {window_size_parm} > /dev/null 2>&1 &')
os.environ['DISPLAY'] = display_num
if wait:
time.sleep(wait)
@@ -0,0 +1,128 @@
"""PyVista volume module."""
from __future__ import annotations
from typing import TYPE_CHECKING
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk
from .prop3d import Prop3D
if TYPE_CHECKING:
from typing_extensions import Self
from .mapper import _BaseMapper
from .volume_property import VolumeProperty
class Volume(Prop3D, _vtk.vtkVolume):
"""Wrapper class for VTK volume.
This class represents a volume in a rendered scene. It inherits
functions related to the volume's position, orientation and origin
from Prop3D.
"""
def __init__(self):
"""Initialize volume."""
super().__init__()
@property
def mapper(self) -> _BaseMapper: # numpydoc ignore=RT01
"""Return or set the mapper of the volume.
Examples
--------
Add a volume to a :class:`pyvista.Plotter` and get its mapper.
>>> import pyvista as pv
>>> vol = pv.ImageData(dimensions=(10, 10, 10))
>>> vol['scalars'] = 255 - vol.z * 25
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(vol)
>>> actor.mapper.bounds
BoundsTuple(x_min = 0.0,
x_max = 9.0,
y_min = 0.0,
y_max = 9.0,
z_min = 0.0,
z_max = 9.0)
"""
return self.GetMapper() # type: ignore[return-value]
@mapper.setter
def mapper(self, obj):
self.SetMapper(obj)
@property
def prop(self): # numpydoc ignore=RT01
"""Return or set the property of this actor.
Examples
--------
Create an volume and get its properties.
>>> import pyvista as pv
>>> vol = pv.ImageData(dimensions=(10, 10, 10))
>>> vol['scalars'] = 255 - vol.z * 25
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(vol)
>>> actor.prop.GetShade()
0
"""
return self.GetProperty()
@prop.setter
def prop(self, obj: VolumeProperty):
self.SetProperty(obj)
@_deprecate_positional_args
def copy(self: Self, deep: bool = True) -> Self: # noqa: FBT001, FBT002
"""Create a copy of this volume.
Parameters
----------
deep : bool, default: True
Create a shallow or deep copy of the volume. A deep copy will have a
new property and mapper, while a shallow copy will use the mapper
and property of this volume.
Returns
-------
Volume
Deep or shallow copy of this volume.
Examples
--------
Create a volume of by adding it to a :class:`~pyvista.Plotter`
and then copy the volume.
>>> import pyvista as pv
>>> mesh = pv.Wavelet()
>>> pl = pv.Plotter()
>>> volume = pl.add_volume(mesh, diffuse=0.5)
>>> new_volume = volume.copy()
Change the copy's properties. A deep copy is made by default, so the original
volume is not affected.
>>> new_volume.prop.diffuse = 1.0
>>> new_volume.prop.diffuse
1.0
>>> volume.prop.diffuse
0.5
"""
new_actor = type(self)()
if deep:
if self.mapper is not None:
new_actor.mapper = self.mapper.copy()
new_actor.prop = self.prop.copy()
else:
new_actor.ShallowCopy(self)
return new_actor
@@ -0,0 +1,414 @@
"""Wrapper for :vtk:`vtkVolumeProperty`."""
from __future__ import annotations
import weakref
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
class VolumeProperty(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkVolumeProperty):
"""Wrap the VTK class :vtk:`vtkVolumeProperty`.
This class is used to represent common properties associated with volume
rendering. This includes properties for determining the type of
interpolation to use when sampling a volume, the color of a volume, the
scalar opacity of a volume, the gradient opacity of a volume, and the
shading parameters of a volume.
Parameters
----------
lookup_table : pyvista.LookupTable, optional
Lookup table to set the color and opacity transfer functions.
interpolation_type : str, optional
Value must be either ``'linear'`` or ``'nearest'``.
ambient : float, optional
When lighting is enabled, this is the amount of light in
the range of 0 to 1 (default 0.0) that reaches the actor
when not directed at the light source emitted from the
viewer.
diffuse : float, optional
The diffuse lighting coefficient.
specular : float, optional
The specular lighting coefficient.
specular_power : float, optional
The specular power. Between 0.0 and 128.0.
shade : bool, optional
Enable or disable volume shading. If shading is turned off, then the
mapper for the volume will not perform shading calculations. If shading
is turned on, the mapper may perform shading calculations - in some
cases shading does not apply (for example, in a maximum intensity
projection) and therefore shading will not be performed even if this
flag is on. For a compositing type of mapper, turning shading off is
generally the same as setting ``ambient=1``, ``diffuse=0``,
``specular=0``. Shading can be independently turned on/off per
component.
opacity_unit_distance : float, optional
This is the unit distance on which the scalar opacity transfer function
is defined. By default this is 1.0, meaning that over a distance of 1.0
units, a given opacity (from the transfer function) is
accumulated. This is adjusted for the actual sampling distance during
rendering.
Examples
--------
Create a sample dataset from perlin noise and apply a lookup table to the
:class:`VolumeProperty`.
>>> import pyvista as pv
>>> noise = pv.perlin_noise(1, (1, 3, 5), (0, 0, 0))
>>> grid = pv.sample_function(
... noise, bounds=[0, 3.0, -0, 1.0, 0, 1.0], dim=(40, 40, 40)
... )
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(grid, show_scalar_bar=False)
>>> lut = actor.mapper.lookup_table
>>> lut.cmap = 'bwr'
>>> lut.apply_opacity([1.0, 0.0, 0.0, 0.3, 0.0, 0.0, 0.0, 0.3])
>>> pl.show()
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self,
lookup_table=None,
interpolation_type=None,
ambient=None,
diffuse=None,
specular=None,
specular_power=None,
shade=None,
opacity_unit_distance=None,
):
"""Initialize the :vtk:`vtkVolumeProperty` class."""
super().__init__()
self._lookup_table_ = None
self._lookup_table_observer_id = None
if lookup_table is not None:
self.apply_lookup_table(lookup_table)
if interpolation_type is not None:
self.interpolation_type = interpolation_type
if ambient is not None:
self.ambient = ambient
if diffuse is not None:
self.diffuse = diffuse
if specular is not None:
self.specular = specular
if specular_power is not None:
self.specular_power = specular_power
if shade is not None:
self.shade = shade
if opacity_unit_distance is not None:
self.opacity_unit_distance = opacity_unit_distance
@property
def _lookup_table(self) -> pyvista.LookupTable | None:
"""Get the lookup table if applied via apply_lookup_table."""
if self._lookup_table_ is not None:
return self._lookup_table_()
return None
@_lookup_table.setter
def _lookup_table(self, lookup_table: pyvista.LookupTable):
"""Set the lookup table if applied via apply_lookup_table."""
if self._lookup_table is not None and self._lookup_table_observer_id is not None:
# Clean up the old lookup table observer
self._lookup_table.RemoveObserver(self._lookup_table_observer_id)
self._lookup_table_observer_id = None
self._lookup_table_ = weakref.ref(lookup_table)
self._lookup_table_observer_id = lookup_table.AddObserver(
_vtk.vtkCommand.ModifiedEvent,
lambda *_: self.reapply_lookup_table(),
)
def reapply_lookup_table(self):
"""Reapply the lookup table previously applied.
The VolumeProperty is unable to keep a dynamic link to the colors
and mapping laid out in the lookup table. This method allows you to
reapply the lookup table to the VolumeProperty. This is useful if
you modify the lookup table after it is applied to the
VolumeProperty.
We have our own modified event observer to reapply this automatically
when the lookup table is modified.
"""
if self._lookup_table is not None:
self.apply_lookup_table(self._lookup_table)
def apply_lookup_table(self, lookup_table: pyvista.LookupTable):
"""Apply a lookup table to the volume property.
Applies both the color and opacity of the lookup table as transfer
functions.
Parameters
----------
lookup_table : pyvista.LookupTable, optional
Lookup table to set the color and opacity transfer functions.
Examples
--------
Plot perlin noise volumetrically using a custom lookup table.
>>> import pyvista as pv
>>> noise = pv.perlin_noise(1, (1, 3, 5), (0, 0, 0))
>>> grid = pv.sample_function(
... noise, bounds=[0, 3.0, -0, 1.0, 0, 1.0], dim=(40, 40, 40)
... )
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(grid, show_scalar_bar=False)
>>> lut = actor.mapper.lookup_table
>>> lut.cmap = 'bwr'
>>> lut.apply_opacity([1.0, 0.0, 0.0, 0.3, 0.0, 0.0, 0.0, 0.3])
>>> pl.show()
"""
if not isinstance(lookup_table, pyvista.LookupTable):
msg = '`lookup_table` must be a `pyvista.LookupTable`'
raise TypeError(msg)
if self._lookup_table != lookup_table:
self._lookup_table = lookup_table
self.SetColor(lookup_table.to_color_tf())
self.SetScalarOpacity(lookup_table.to_opacity_tf())
def __del__(self):
"""Clean up the lookup table observer when the object is deleted."""
if self._lookup_table_observer_id is not None and self._lookup_table is not None:
self._lookup_table.RemoveObserver(self._lookup_table_observer_id)
self._lookup_table_observer_id = None
self._lookup_table_ = None
@property
def interpolation_type(self) -> str: # numpydoc ignore=RT01
"""Return or set the interpolation type.
Value must be either ``'linear'`` or ``'nearest'``.
Examples
--------
Create a sample :class:`pyvista.ImageData` dataset.
>>> import numpy as np
>>> import pyvista as pv
>>> n = 21
>>> c = -(n - 1) / 2
>>> vol = pv.ImageData(dimensions=(n, n, n), origin=(c, c, c))
>>> scalars = np.linalg.norm(vol.points, axis=1)
>>> scalars *= 255 / scalars.max()
>>> vol['scalars'] = scalars
Demonstrate nearest (default) interpolation.
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(
... vol,
... show_scalar_bar=False,
... opacity=[0.3, 0.0, 0.05, 0.0, 0.0, 0.0, 1.0, 0.0],
... cmap='plasma',
... )
>>> actor.prop.interpolation_type = 'nearest'
>>> pl.show()
Demonstrate linear interpolation.
>>> pl = pv.Plotter()
>>> actor = pl.add_volume(
... vol,
... show_scalar_bar=False,
... opacity=[0.3, 0.0, 0.05, 0.0, 0.0, 0.0, 1.0, 0.0],
... cmap='plasma',
... )
>>> actor.prop.interpolation_type = 'linear'
>>> pl.show()
"""
return self.GetInterpolationTypeAsString().split()[0].lower()
@interpolation_type.setter
def interpolation_type(self, value: str):
if value == 'linear':
self.SetInterpolationTypeToLinear()
elif value == 'nearest':
self.SetInterpolationTypeToNearest()
else:
msg = '`interpolation_type` must be either "linear" or "nearest"'
raise ValueError(msg)
@property
def opacity_unit_distance(self) -> float: # numpydoc ignore=RT01
"""Return or set the opacity unit distance.
This is the unit distance on which the scalar opacity transfer function
is defined.
By default this is 1.0, meaning that over a distance of 1.0 units, a
given opacity (from the transfer function) is accumulated. This is
adjusted for the actual sampling distance during rendering.
"""
return self.GetScalarOpacityUnitDistance()
@opacity_unit_distance.setter
def opacity_unit_distance(self, value: float):
self.SetScalarOpacityUnitDistance(value)
@property
def shade(self) -> bool: # numpydoc ignore=RT01
"""Return or set shading of a volume.
If shading is turned off, then the mapper for the volume will not
perform shading calculations. If shading is turned on, the mapper may
perform shading calculations - in some cases shading does not apply
(for example, in a maximum intensity projection) and therefore shading
will not be performed even if this flag is on. For a compositing type
of mapper, turning shading off is generally the same as setting
``ambient=1``, ``diffuse=0``, ``specular=0``. Shading can be
independently turned on/off per component.
"""
return bool(self.GetShade())
@shade.setter
def shade(self, value: bool):
self.SetShade(value)
@property
def independent_components(self) -> bool: # numpydoc ignore=RT01
"""Return or set independent components.
If ``False``, then you must have either 2 or 4 component data.
For 2 component data, the first is passed through the
first color transfer function and the second component is passed
through the first scalar opacity (and gradient opacity) transfer
function. Normals will be generated off of the second component. When
using gradient based opacity modulation, the gradients are computed off
of the second component.
For 4 component data, the first three will directly represent RGB (no
lookup table). The fourth component will be passed through the first
scalar opacity transfer function for opacity and first gradient opacity
transfer function for gradient based opacity modulation. Normals will
be generated from the fourth component. When using gradient based
opacity modulation, the gradients are computed off of the fourth
component.
"""
return bool(self.GetIndependentComponents())
@independent_components.setter
def independent_components(self, value: bool):
self.SetIndependentComponents(value)
@property
def ambient(self) -> float: # numpydoc ignore=RT01
"""Return or set ambient lighting coefficient.
This is the amount of light in the range of 0 to 1 (default 0.0) that
reaches the actor when not directed at the light source emitted from
the viewer.
Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
set to ``True``.
"""
return self.GetAmbient()
@ambient.setter
def ambient(self, value: float):
self.SetAmbient(value)
@property
def diffuse(self) -> float: # numpydoc ignore=RT01
"""Return or set the diffuse lighting coefficient.
This is the scattering of light by reflection or transmission. Diffuse
reflection results when light strikes an irregular surface such as a
frosted window or the surface of a frosted or coated light bulb.
Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
set to ``True``.
"""
return self.GetDiffuse()
@diffuse.setter
def diffuse(self, value: float):
self.SetDiffuse(value)
@property
def specular(self) -> float: # numpydoc ignore=RT01
"""Return or set specular.
Default 0.0
Specular lighting simulates the bright spot of a light that appears on
shiny objects.
Changing attribute has no effect unless :attr:`VolumeProperty.shade` is
set to ``True``.
"""
return self.GetSpecular()
@specular.setter
def specular(self, value: float):
self.SetSpecular(value)
@property
def specular_power(self) -> float: # numpydoc ignore=RT01
"""Return or set specular power.
The specular power. Between 0.0 and 128.0. Default 10.0
"""
return self.GetSpecularPower()
@specular_power.setter
def specular_power(self, value: float):
self.SetSpecularPower(value)
def copy(self) -> VolumeProperty:
"""Create a deep copy of this property.
Returns
-------
pyvista.plotting.volume_property.VolumeProperty
Deep copy of this property.
"""
new_prop = VolumeProperty()
new_prop.DeepCopy(self)
return new_prop
def __repr__(self):
"""Representation of this property."""
props = [
f'{type(self).__name__} ({hex(id(self))})',
]
for attr in dir(self):
if not attr.startswith('_') and attr[0].islower():
name = ' '.join(attr.split('_')).capitalize() + ':'
try:
value = getattr(self, attr)
except AttributeError: # pragma:no cover
continue
if callable(value):
continue
if isinstance(value, str):
value = f'"{value}"'
props.append(f' {name:28s} {value}')
return '\n'.join(props)
File diff suppressed because it is too large Load Diff