init
This commit is contained in:
@@ -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
Reference in New Issue
Block a user