2096 lines
72 KiB
Python
2096 lines
72 KiB
Python
"""Axes assembly module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import abstractmethod
|
|
from collections.abc import Sequence
|
|
import itertools
|
|
from typing import TYPE_CHECKING
|
|
from typing import Any
|
|
from typing import Literal
|
|
from typing import NamedTuple
|
|
from typing import TypedDict
|
|
|
|
import numpy as np
|
|
|
|
import pyvista as pv
|
|
from pyvista import BoundsTuple
|
|
from pyvista._deprecate_positional_args import _deprecate_positional_args
|
|
from pyvista.core import _validation
|
|
from pyvista.core._validation.validate import _validate_color_sequence
|
|
from pyvista.core.utilities.geometric_sources import AxesGeometrySource
|
|
from pyvista.core.utilities.geometric_sources import OrthogonalPlanesSource
|
|
from pyvista.core.utilities.geometric_sources import _AxisEnum
|
|
from pyvista.core.utilities.geometric_sources import _PartEnum
|
|
from pyvista.core.utilities.misc import _NameMixin
|
|
from pyvista.core.utilities.misc import _NoNewAttrMixin
|
|
from pyvista.core.utilities.misc import abstract_class
|
|
from pyvista.plotting import _vtk
|
|
from pyvista.plotting.actor import Actor
|
|
from pyvista.plotting.colors import Color
|
|
from pyvista.plotting.prop3d import Prop3D
|
|
from pyvista.plotting.prop3d import _Prop3DMixin
|
|
from pyvista.plotting.text import Label
|
|
from pyvista.plotting.text import TextProperty
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
import sys
|
|
|
|
from pyvista.core._typing_core import MatrixLike
|
|
from pyvista.core._typing_core import VectorLike
|
|
from pyvista.core.dataset import DataSet
|
|
from pyvista.plotting._typing import ColorLike
|
|
|
|
if sys.version_info >= (3, 11):
|
|
from typing import Unpack
|
|
else:
|
|
from typing_extensions import Unpack
|
|
|
|
|
|
class _AxesPropTuple(NamedTuple):
|
|
x_shaft: float | str | ColorLike
|
|
y_shaft: float | str | ColorLike
|
|
z_shaft: float | str | ColorLike
|
|
x_tip: float | str | ColorLike
|
|
y_tip: float | str | ColorLike
|
|
z_tip: float | str | ColorLike
|
|
|
|
|
|
class _AxesGeometryKwargs(TypedDict):
|
|
shaft_type: AxesGeometrySource.GeometryTypes | DataSet
|
|
shaft_radius: float
|
|
shaft_length: float | VectorLike[float]
|
|
tip_type: AxesGeometrySource.GeometryTypes | DataSet
|
|
tip_radius: float
|
|
tip_length: float | VectorLike[float]
|
|
symmetric_bounds: bool
|
|
|
|
|
|
class _OrthogonalPlanesKwargs(TypedDict):
|
|
bounds: VectorLike[float]
|
|
resolution: int | VectorLike[int]
|
|
normal_sign: Literal['+', '-'] | Sequence[str]
|
|
|
|
|
|
class _XYZTuple(NamedTuple):
|
|
x: Any
|
|
y: Any
|
|
z: Any
|
|
|
|
|
|
@abstract_class
|
|
class _XYZAssembly(
|
|
_NoNewAttrMixin,
|
|
_vtk.DisableVtkSnakeCase,
|
|
_Prop3DMixin,
|
|
_NameMixin,
|
|
_vtk.vtkPropAssembly,
|
|
):
|
|
DEFAULT_LABELS = _XYZTuple('X', 'Y', 'Z')
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
xyz_actors: tuple[Any, Any, Any],
|
|
xyz_label_actors: tuple[Any, Any, Any],
|
|
x_label,
|
|
y_label,
|
|
z_label,
|
|
labels,
|
|
label_color,
|
|
show_labels,
|
|
label_position,
|
|
label_size,
|
|
x_color,
|
|
y_color,
|
|
z_color,
|
|
position: VectorLike[float],
|
|
orientation: VectorLike[float],
|
|
origin: VectorLike[float],
|
|
scale: float | VectorLike[float],
|
|
user_matrix: MatrixLike[float] | None,
|
|
name: str | None = None,
|
|
):
|
|
super().__init__()
|
|
|
|
def _make_xyz_tuple(xyz):
|
|
def _get_tuple(actor_or_actors):
|
|
return (
|
|
actor_or_actors if isinstance(actor_or_actors, tuple) else (actor_or_actors,)
|
|
)
|
|
|
|
actor_tuples = [_get_tuple(actors) for actors in xyz]
|
|
return _XYZTuple(*actor_tuples)
|
|
|
|
self._assembly_actors = _make_xyz_tuple(xyz_actors)
|
|
self._assembly_label_actors = _make_xyz_tuple(xyz_label_actors)
|
|
|
|
# Add all actors to assembly
|
|
for parts in (*self._assembly_actors, *self._assembly_label_actors):
|
|
for part in parts:
|
|
self.AddPart(part)
|
|
|
|
# Set colors
|
|
if x_color is None:
|
|
x_color = pv.global_theme.axes.x_color
|
|
if y_color is None:
|
|
y_color = pv.global_theme.axes.y_color
|
|
if z_color is None:
|
|
z_color = pv.global_theme.axes.z_color
|
|
|
|
self.x_color = x_color
|
|
self.y_color = y_color
|
|
self.z_color = z_color
|
|
|
|
# Set text labels
|
|
if labels is None:
|
|
self.x_label = self.DEFAULT_LABELS.x if x_label is None else x_label
|
|
self.y_label = self.DEFAULT_LABELS.y if y_label is None else y_label
|
|
self.z_label = self.DEFAULT_LABELS.z if z_label is None else z_label
|
|
else:
|
|
msg = (
|
|
"Cannot initialize '{}' and 'labels' properties together. "
|
|
'Specify one or the other, not both.'
|
|
)
|
|
if x_label is not None:
|
|
raise ValueError(msg.format('x_label'))
|
|
if y_label is not None:
|
|
raise ValueError(msg.format('y_label'))
|
|
if z_label is not None:
|
|
raise ValueError(msg.format('z_label'))
|
|
self.labels = labels
|
|
self.show_labels = show_labels
|
|
self.label_color = label_color
|
|
self.label_size = label_size
|
|
self.label_position = label_position
|
|
|
|
self.position = position
|
|
self.orientation = orientation
|
|
self.scale = scale # type: ignore[assignment]
|
|
self.origin = origin
|
|
self.user_matrix = user_matrix
|
|
|
|
self._name = name
|
|
|
|
@property
|
|
def parts(self):
|
|
collection = self.GetParts()
|
|
return tuple(collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems()))
|
|
|
|
@property
|
|
def _label_actor_iterator(self) -> Iterator[Label]:
|
|
return itertools.chain.from_iterable(self._assembly_label_actors)
|
|
|
|
def _post_set_update(self):
|
|
# Update prop3D attributes for all assembly parts
|
|
parts = self.parts
|
|
new_matrix = pv.array_from_vtkmatrix(self._prop3d.GetMatrix())
|
|
for part in parts:
|
|
if isinstance(part, (Prop3D, _Prop3DMixin)) and not np.array_equal(
|
|
part.user_matrix, new_matrix
|
|
):
|
|
part.user_matrix = new_matrix
|
|
|
|
def _get_bounds(self) -> BoundsTuple: # numpydoc ignore=RT01
|
|
return BoundsTuple(*self.GetBounds())
|
|
|
|
@property
|
|
def show_labels(self) -> bool: # numpydoc ignore=RT01
|
|
"""Show or hide the text labels for the axes."""
|
|
return self._show_labels
|
|
|
|
@show_labels.setter
|
|
def show_labels(self, value: bool):
|
|
self._show_labels = value
|
|
for label in self._label_actor_iterator:
|
|
label.SetVisibility(value)
|
|
|
|
@property
|
|
@abstractmethod
|
|
def labels(self): # numpydoc ignore=RT01
|
|
"""XYZ labels."""
|
|
|
|
@labels.setter
|
|
@abstractmethod
|
|
def labels(self, labels):
|
|
"""XYZ labels."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def x_label(self): # numpydoc ignore=RT01
|
|
"""Text label for the x-axis."""
|
|
|
|
@x_label.setter
|
|
@abstractmethod
|
|
def x_label(self, label):
|
|
"""Text label for the x-axis."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def y_label(self): # numpydoc ignore=RT01
|
|
"""Text label for the y-axis."""
|
|
|
|
@y_label.setter
|
|
@abstractmethod
|
|
def y_label(self, label):
|
|
"""Text label for the y-axis."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def z_label(self): # numpydoc ignore=RT01
|
|
"""Text label for the z-axis."""
|
|
|
|
@z_label.setter
|
|
@abstractmethod
|
|
def z_label(self, label):
|
|
"""Text label for the z-axis."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def label_size(self): # numpydoc ignore=RT01
|
|
"""Size of the text labels."""
|
|
|
|
@label_size.setter
|
|
@abstractmethod
|
|
def label_size(self, size):
|
|
"""Size of the text labels."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def label_position(self): # numpydoc ignore=RT01
|
|
"""Position of the text labels."""
|
|
|
|
@label_position.setter
|
|
@abstractmethod
|
|
def label_position(self, position):
|
|
"""Position of the text labels."""
|
|
|
|
@property
|
|
def label_color(self) -> Color: # numpydoc ignore=RT01
|
|
"""Color of the text labels."""
|
|
return self._label_color
|
|
|
|
@label_color.setter
|
|
def label_color(self, color: ColorLike):
|
|
valid_color = Color(color)
|
|
self._label_color = valid_color
|
|
for label in self._label_actor_iterator:
|
|
label.prop.color = valid_color
|
|
|
|
@property
|
|
@abstractmethod
|
|
def x_color(self): # numpydoc ignore=RT01
|
|
"""Color of the x-axis actors."""
|
|
|
|
@x_color.setter
|
|
@abstractmethod
|
|
def x_color(self, color):
|
|
"""Color of the x-axis actors."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def y_color(self): # numpydoc ignore=RT01
|
|
"""Color of the y-axis actors."""
|
|
|
|
@y_color.setter
|
|
@abstractmethod
|
|
def y_color(self, color):
|
|
"""Color of the y-axis actors."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def z_color(self): # numpydoc ignore=RT01
|
|
"""Color of the z-axis actors."""
|
|
|
|
@z_color.setter
|
|
@abstractmethod
|
|
def z_color(self, color):
|
|
"""Color of the z-axis actors."""
|
|
|
|
|
|
class AxesAssembly(_XYZAssembly):
|
|
"""Assembly of arrow-style axes parts.
|
|
|
|
The axes may be used as a widget or added to a scene.
|
|
|
|
Parameters
|
|
----------
|
|
x_label : str, default: 'X'
|
|
Text label for the x-axis. Alternatively, set the label with :attr:`labels`.
|
|
|
|
y_label : str, default: 'Y'
|
|
Text label for the y-axis. Alternatively, set the label with :attr:`labels`.
|
|
|
|
z_label : str, default: 'Z'
|
|
Text label for the z-axis. Alternatively, set the label with :attr:`labels`.
|
|
|
|
labels : Sequence[str], optional,
|
|
Text labels for the axes. This is an alternative parameter to using
|
|
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
label_color : ColorLike, default: 'black'
|
|
Color of the text labels.
|
|
|
|
show_labels : bool, default: True
|
|
Show or hide the text labels.
|
|
|
|
label_position : float | VectorLike[float], optional
|
|
Position of the text labels along each axis. By default, the labels are
|
|
positioned at the ends of the shafts.
|
|
|
|
label_size : int, default: 50
|
|
Size of the text labels.
|
|
|
|
x_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the x-axis shaft and tip.
|
|
|
|
y_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the y-axis shaft and tip.
|
|
|
|
z_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the z-axis shaft and tip.
|
|
|
|
position : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Position of the axes in space.
|
|
|
|
orientation : VectorLike[float], default: (0, 0, 0)
|
|
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
|
|
around the y-axis first, then the x-axis, and finally the z-axis.
|
|
|
|
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Origin of the axes. This is the point about which all rotations take place. The
|
|
rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
|
|
|
|
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
|
|
Scaling factor applied to the axes.
|
|
|
|
user_matrix : MatrixLike[float], optional
|
|
A 4x4 transformation matrix applied to the axes. Defaults to the identity matrix.
|
|
The user matrix is the last transformation applied to the actor.
|
|
|
|
name : str, optional
|
|
The name of this assembly used when tracking on a plotter.
|
|
|
|
.. versionadded:: 0.45
|
|
|
|
**kwargs
|
|
Keyword arguments passed to :class:`pyvista.AxesGeometrySource`.
|
|
|
|
See Also
|
|
--------
|
|
AxesAssemblySymmetric
|
|
|
|
:ref:`axes_objects_example`
|
|
Example showing different axes objects.
|
|
|
|
Examples
|
|
--------
|
|
Add axes to a plot.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes = pv.AxesAssembly()
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes)
|
|
>>> pl.show()
|
|
|
|
Customize the axes colors. Set each axis to a single color, or set the colors of
|
|
each shaft and tip separately with two colors.
|
|
|
|
>>> axes.x_color = ['cyan', 'blue']
|
|
>>> axes.y_color = ['magenta', 'red']
|
|
>>> axes.z_color = 'yellow'
|
|
|
|
Customize the label color too.
|
|
|
|
>>> axes.label_color = 'brown'
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes)
|
|
>>> pl.show()
|
|
|
|
Create axes with custom geometry. Use pyramid shafts and hemisphere tips and
|
|
modify the lengths.
|
|
|
|
>>> axes = pv.AxesAssembly(
|
|
... shaft_type='pyramid',
|
|
... tip_type='hemisphere',
|
|
... tip_length=0.1,
|
|
... shaft_length=(0.5, 1.0, 1.5),
|
|
... )
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes)
|
|
>>> pl.show()
|
|
|
|
Position and orient the axes in space.
|
|
|
|
>>> axes = pv.AxesAssembly(position=(1.0, 2.0, 3.0), orientation=(10, 20, 30))
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes)
|
|
>>> pl.show()
|
|
|
|
Add the axes as a custom orientation widget with
|
|
:func:`~pyvista.Renderer.add_orientation_widget`:
|
|
|
|
>>> import pyvista as pv
|
|
|
|
>>> axes = pv.AxesAssembly(symmetric_bounds=True)
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_mesh(pv.Cone())
|
|
>>> _ = pl.add_orientation_widget(
|
|
... axes,
|
|
... viewport=(0, 0, 0.5, 0.5),
|
|
... )
|
|
>>> pl.show()
|
|
|
|
"""
|
|
|
|
def _init_actors_from_source(self, geometry_source: AxesGeometrySource):
|
|
# Init shaft and tip actors
|
|
self._shaft_actors: tuple[Actor, Actor, Actor] = (Actor(), Actor(), Actor())
|
|
self._tip_actors: tuple[Actor, Actor, Actor] = (Actor(), Actor(), Actor())
|
|
self._shaft_and_tip_actors = (*self._shaft_actors, *self._tip_actors)
|
|
|
|
# Init shaft and tip datasets
|
|
self._shaft_and_tip_geometry_source = geometry_source
|
|
shaft_tip_datasets = self._shaft_and_tip_geometry_source.output
|
|
for actor, dataset in zip(self._shaft_and_tip_actors, shaft_tip_datasets):
|
|
actor.mapper = pv.DataSetMapper(dataset=dataset)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
x_label: str | None = None,
|
|
y_label: str | None = None,
|
|
z_label: str | None = None,
|
|
labels: Sequence[str] | None = None,
|
|
label_color: ColorLike = 'black',
|
|
show_labels: bool = True,
|
|
label_position: float | VectorLike[float] | None = None,
|
|
label_size: int = 50,
|
|
x_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
y_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
z_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
position: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
origin: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
|
|
user_matrix: MatrixLike[float] | None = None,
|
|
name: str | None = None,
|
|
**kwargs: Unpack[_AxesGeometryKwargs],
|
|
):
|
|
# Init shaft and tip actors
|
|
self._init_actors_from_source(AxesGeometrySource(symmetric=False, **kwargs))
|
|
# Init label actors
|
|
self._label_actors = (Label(), Label(), Label())
|
|
|
|
_XYZAssembly.__init__(
|
|
self,
|
|
xyz_actors=tuple(zip(self._shaft_actors, self._tip_actors)), # type: ignore[arg-type]
|
|
xyz_label_actors=self._label_actors,
|
|
x_label=x_label,
|
|
y_label=y_label,
|
|
z_label=z_label,
|
|
labels=labels,
|
|
label_color=label_color,
|
|
show_labels=show_labels,
|
|
label_position=label_position,
|
|
label_size=label_size,
|
|
x_color=x_color,
|
|
y_color=y_color,
|
|
z_color=z_color,
|
|
position=position,
|
|
orientation=orientation,
|
|
origin=origin,
|
|
scale=scale,
|
|
user_matrix=user_matrix,
|
|
name=name,
|
|
)
|
|
self._set_default_label_props()
|
|
|
|
def _set_default_label_props(self):
|
|
# TODO: implement set_text_prop() and use that instead
|
|
for label in self._label_actor_iterator:
|
|
prop = label.prop
|
|
prop.bold = True
|
|
prop.italic = True
|
|
prop.enable_shadow()
|
|
prop.SetShadowOffset(-1, 1)
|
|
|
|
def __repr__(self):
|
|
"""Representation of the axes assembly."""
|
|
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
|
|
bnds = self.bounds
|
|
|
|
geometry_repr = repr(self._shaft_and_tip_geometry_source).splitlines()[1:]
|
|
|
|
attr = [
|
|
f'{type(self).__name__} ({hex(id(self))})',
|
|
*geometry_repr,
|
|
f" X label: '{self.x_label}'",
|
|
f" Y label: '{self.y_label}'",
|
|
f" Z label: '{self.z_label}'",
|
|
f' Label color: {self.label_color}',
|
|
f' Show labels: {self.show_labels}',
|
|
f' Label position: {self.label_position}',
|
|
' X Color: ',
|
|
f' Shaft {self.x_color[0]}',
|
|
f' Tip {self.x_color[1]}',
|
|
' Y Color: ',
|
|
f' Shaft {self.y_color[0]}',
|
|
f' Tip {self.y_color[1]}',
|
|
' Z Color: ',
|
|
f' Shaft {self.z_color[0]}',
|
|
f' Tip {self.z_color[1]}',
|
|
f' Position: {self.position}',
|
|
f' Orientation: {self.orientation}',
|
|
f' Origin: {self.origin}',
|
|
f' Scale: {self.scale}',
|
|
f' User matrix: {mat_info}',
|
|
f' X Bounds {bnds.x_min:.3E}, {bnds.x_max:.3E}',
|
|
f' Y Bounds {bnds.y_min:.3E}, {bnds.y_max:.3E}',
|
|
f' Z Bounds {bnds.z_min:.3E}, {bnds.z_max:.3E}',
|
|
]
|
|
return '\n'.join(attr)
|
|
|
|
@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_label`,
|
|
:attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.labels = ['X Axis', 'Y Axis', 'Z Axis']
|
|
>>> axes_assembly.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, str, str]):
|
|
labels = _validate_label_sequence(labels, n_labels=3, name='labels')
|
|
self.x_label = labels[0]
|
|
self.y_label = labels[1]
|
|
self.z_label = labels[2]
|
|
|
|
@property
|
|
def x_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the x-axis.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.x_label = 'This axis'
|
|
>>> axes_assembly.x_label
|
|
'This axis'
|
|
|
|
"""
|
|
return self._label_actors[0].input
|
|
|
|
@x_label.setter
|
|
def x_label(self, label: str):
|
|
self._label_actors[0].input = label
|
|
|
|
@property
|
|
def y_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the y-axis.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.y_label = 'This axis'
|
|
>>> axes_assembly.y_label
|
|
'This axis'
|
|
|
|
"""
|
|
return self._label_actors[1].input
|
|
|
|
@y_label.setter
|
|
def y_label(self, label: str):
|
|
self._label_actors[1].input = label
|
|
|
|
@property
|
|
def z_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the z-axis.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.z_label = 'This axis'
|
|
>>> axes_assembly.z_label
|
|
'This axis'
|
|
|
|
"""
|
|
return self._label_actors[2].input
|
|
|
|
@z_label.setter
|
|
def z_label(self, label: str):
|
|
self._label_actors[2].input = label
|
|
|
|
@property
|
|
def label_size(self) -> int: # numpydoc ignore=RT01
|
|
"""Size of the text labels.
|
|
|
|
Must be a positive integer.
|
|
"""
|
|
return self._label_size
|
|
|
|
@label_size.setter
|
|
def label_size(self, size: int):
|
|
self._label_size = size
|
|
for label in self._label_actor_iterator:
|
|
label.size = size
|
|
|
|
@property
|
|
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
|
|
"""Position of the text label along each axis.
|
|
|
|
By default, the labels are positioned at the ends of the shafts.
|
|
|
|
Values must be non-negative.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.label_position
|
|
(0.8, 0.8, 0.8)
|
|
>>> axes_assembly.label_position = 0.3
|
|
>>> axes_assembly.label_position
|
|
(0.3, 0.3, 0.3)
|
|
>>> axes_assembly.label_position = (0.1, 0.4, 0.2)
|
|
>>> axes_assembly.label_position
|
|
(0.1, 0.4, 0.2)
|
|
|
|
"""
|
|
position = self._label_position
|
|
return self._shaft_and_tip_geometry_source.shaft_length if position is None else position
|
|
|
|
@label_position.setter
|
|
def label_position(self, position: float | VectorLike[float] | None):
|
|
self._label_position = (
|
|
None
|
|
if position is None
|
|
else _validation.validate_array3(
|
|
position,
|
|
broadcast=True,
|
|
must_be_in_range=[0, np.inf],
|
|
name='Label position',
|
|
dtype_out=float,
|
|
to_tuple=True,
|
|
)
|
|
)
|
|
self._update_label_positions()
|
|
|
|
@property
|
|
def x_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
|
|
"""Color of the x-axis shaft and tip."""
|
|
return self.get_actor_prop('color')[_AxisEnum.x :: 3]
|
|
|
|
@x_color.setter
|
|
def x_color(self, color: ColorLike | Sequence[ColorLike]):
|
|
self.set_actor_prop('color', color, axis=_AxisEnum.x.value) # type: ignore[arg-type]
|
|
|
|
@property
|
|
def y_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
|
|
"""Color of the y-axis shaft and tip."""
|
|
return self.get_actor_prop('color')[_AxisEnum.y :: 3]
|
|
|
|
@y_color.setter
|
|
def y_color(self, color: ColorLike | Sequence[ColorLike]):
|
|
self.set_actor_prop('color', color, axis=_AxisEnum.y.value) # type: ignore[arg-type]
|
|
|
|
@property
|
|
def z_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
|
|
"""Color of the z-axis shaft and tip."""
|
|
return self.get_actor_prop('color')[_AxisEnum.z.value :: 3]
|
|
|
|
@z_color.setter
|
|
def z_color(self, color: ColorLike | Sequence[ColorLike]):
|
|
self.set_actor_prop('color', color, axis=_AxisEnum.z.value) # type: ignore[arg-type]
|
|
|
|
@_deprecate_positional_args(allowed=['name', 'value'])
|
|
def set_actor_prop( # noqa: PLR0917
|
|
self,
|
|
name: str,
|
|
value: float | str | ColorLike | Sequence[float | str | ColorLike],
|
|
axis: Literal['x', 'y', 'z', 'all'] = 'all',
|
|
part: Literal['shaft', 'tip', 'all'] = 'all',
|
|
):
|
|
"""Set :class:`~pyvista.Property` attributes for the axes shaft and/or tip actors.
|
|
|
|
This is a generalized setter method which sets the value of a specific
|
|
:class:`~pyvista.Property` attribute for any combination of axis shaft or tip
|
|
parts.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Name of the :class:`~pyvista.Property` attribute to set.
|
|
|
|
value : float | str | ColorLike | Sequence[float | str | ColorLike]
|
|
Value to set the attribute to. If a single value, set all specified axes
|
|
shaft(s) or tip(s) :class:`~pyvista.Property` attributes to this value.
|
|
If a sequence of values, set the specified parts to these values.
|
|
|
|
axis : str | int, default: 'all'
|
|
Set :class:`~pyvista.Property` attributes for a specific part of the axes.
|
|
Specify one of:
|
|
|
|
- ``'x'``: only set the property for the x-axis.
|
|
- ``'y'``: only set the property for the y-axis.
|
|
- ``'z'``: only set the property for the z-axis.
|
|
- ``'all'``: set the property for all three axes.
|
|
|
|
part : str | int, default: 'all'
|
|
Set the property for a specific part of the axes. Specify one of:
|
|
|
|
- ``'shaft'``: only set the property for the axes shafts.
|
|
- ``'tip'``: only set the property for the axes tips.
|
|
- ``'all'``: set the property for axes shafts and tips.
|
|
|
|
Examples
|
|
--------
|
|
Set :attr:`~pyvista.Property.ambient` for all axes shafts and tips to a
|
|
single value.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.set_actor_prop('ambient', 0.7)
|
|
>>> axes_assembly.get_actor_prop('ambient') # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=0.7,
|
|
y_shaft=0.7,
|
|
z_shaft=0.7,
|
|
x_tip=0.7,
|
|
y_tip=0.7,
|
|
z_tip=0.7)
|
|
|
|
Set the property again, but this time set separate values for each part.
|
|
|
|
>>> values = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
|
|
>>> axes_assembly.set_actor_prop('ambient', values)
|
|
>>> axes_assembly.get_actor_prop('ambient') # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=0.1,
|
|
y_shaft=0.2,
|
|
z_shaft=0.3,
|
|
x_tip=0.4,
|
|
y_tip=0.5,
|
|
z_tip=0.6)
|
|
|
|
Set :attr:`~pyvista.Property.opacity` for the x-axis only. The property is set
|
|
for both the axis shaft and tip by default.
|
|
|
|
>>> axes_assembly.set_actor_prop('opacity', 0.5, axis='x')
|
|
>>> axes_assembly.get_actor_prop('opacity') # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=0.5,
|
|
y_shaft=1.0,
|
|
z_shaft=1.0,
|
|
x_tip=0.5,
|
|
y_tip=1.0,
|
|
z_tip=1.0)
|
|
|
|
Set the property again, but this time set separate values for the shaft and tip.
|
|
|
|
>>> axes_assembly.set_actor_prop('opacity', [0.3, 0.7], axis='x')
|
|
>>> axes_assembly.get_actor_prop('opacity') # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=0.3,
|
|
y_shaft=1.0,
|
|
z_shaft=1.0,
|
|
x_tip=0.7,
|
|
y_tip=1.0,
|
|
z_tip=1.0)
|
|
|
|
Set :attr:`~pyvista.Property.show_edges` for the axes shafts only. The property
|
|
is set for all axes by default.
|
|
|
|
>>> axes_assembly.set_actor_prop('show_edges', True, part='shaft')
|
|
>>> axes_assembly.get_actor_prop(
|
|
... 'show_edges'
|
|
... ) # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=True,
|
|
y_shaft=True,
|
|
z_shaft=True,
|
|
x_tip=False,
|
|
y_tip=False,
|
|
z_tip=False)
|
|
|
|
Set the property again, but this time set separate values for each shaft.
|
|
|
|
>>> axes_assembly.set_actor_prop(
|
|
... 'show_edges', [True, False, True], part='shaft'
|
|
... )
|
|
>>> axes_assembly.get_actor_prop(
|
|
... 'show_edges'
|
|
... ) # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft=True,
|
|
y_shaft=False,
|
|
z_shaft=True,
|
|
x_tip=False,
|
|
y_tip=False,
|
|
z_tip=False)
|
|
|
|
Set :attr:`~pyvista.Property.style` for a single axis and specific part.
|
|
|
|
>>> axes_assembly.set_actor_prop('style', 'wireframe', axis='x', part='shaft')
|
|
>>> axes_assembly.get_actor_prop('style') # doctest: +NORMALIZE_WHITESPACE
|
|
_AxesPropTuple(x_shaft='Wireframe',
|
|
y_shaft='Surface',
|
|
z_shaft='Surface',
|
|
x_tip='Surface',
|
|
y_tip='Surface',
|
|
z_tip='Surface')
|
|
|
|
"""
|
|
actors = self._filter_part_actors(axis=axis, part=part)
|
|
values: Sequence[float | str | ColorLike]
|
|
|
|
# Validate input as a sequence of values
|
|
if 'color' in name:
|
|
# Special case for color inputs
|
|
if axis == 'all' and part == 'all':
|
|
n_values = 6
|
|
elif part == 'all':
|
|
n_values = 2
|
|
elif axis == 'all':
|
|
n_values = 3
|
|
else:
|
|
n_values = 1
|
|
values = _validate_color_sequence(value, n_values) # type: ignore[arg-type]
|
|
elif isinstance(value, Sequence) and not isinstance(value, str):
|
|
# Number sequence
|
|
values = value
|
|
else:
|
|
# Scalar number or string
|
|
values = [value] * len(actors)
|
|
|
|
if len(values) != len(actors):
|
|
msg = (
|
|
f'Number of values ({len(values)}) in {value} must match the number of '
|
|
f"actors ({len(actors)}) for axis '{axis}' and part '{part}'"
|
|
)
|
|
raise ValueError(msg)
|
|
|
|
# Sequence is valid, now set values
|
|
for actor, val in zip(actors, values):
|
|
setattr(actor.prop, name, val)
|
|
|
|
def get_actor_prop(self, name: str):
|
|
"""Get :class:`~pyvista.Property` attributes for the axes shaft and/or tip actors.
|
|
|
|
This is a generalized getter method which returns the value of
|
|
a specific :class:`pyvista.Property` attribute for all shafts and tips.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Name of the :class:`~pyvista.Property` attribute to get.
|
|
|
|
Returns
|
|
-------
|
|
tuple
|
|
Named tuple with attribute values for the axes shafts and tips.
|
|
The values are ordered ``(x_shaft, y_shaft, z_shaft, x_tip, y_tip, z_tip)``.
|
|
|
|
Examples
|
|
--------
|
|
Get the ambient property of the axes shafts and tips.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssembly()
|
|
>>> axes_assembly.get_actor_prop('ambient')
|
|
_AxesPropTuple(x_shaft=0.0, y_shaft=0.0, z_shaft=0.0, x_tip=0.0, y_tip=0.0, z_tip=0.0)
|
|
|
|
"""
|
|
prop_values = [getattr(actor.prop, name) for actor in self._shaft_and_tip_actors]
|
|
return _AxesPropTuple(*prop_values)
|
|
|
|
def _filter_part_actors(
|
|
self,
|
|
axis: Literal['x', 'y', 'z', 'all'] = 'all',
|
|
part: Literal['shaft', 'tip', 'all'] = 'all',
|
|
):
|
|
valid_axis = [0, 1, 2, 'x', 'y', 'z', 'all']
|
|
valid_axis_official = valid_axis[3:]
|
|
if axis not in valid_axis:
|
|
msg = f'Axis must be one of {valid_axis_official}.'
|
|
raise ValueError(msg)
|
|
valid_part = [0, 1, 'shaft', 'tip', 'all']
|
|
valid_part_official = valid_part[2:]
|
|
if part not in valid_part:
|
|
msg = f'Part must be one of {valid_part_official}.'
|
|
raise ValueError(msg)
|
|
|
|
# Create ordered list of filtered actors
|
|
# Iterate over parts in <shaft-xyz> then <tip-xyz> order
|
|
actors: list[Actor] = []
|
|
for part_type, axis_num in itertools.product(_PartEnum, _AxisEnum):
|
|
if part in [part_type.name, part_type.value, 'all'] and axis in [
|
|
axis_num.name,
|
|
axis_num.value,
|
|
'all',
|
|
]:
|
|
# Add actor to list
|
|
if part_type == _PartEnum.shaft:
|
|
actors.append(self._shaft_actors[axis_num])
|
|
else:
|
|
actors.append(self._tip_actors[axis_num])
|
|
|
|
return actors
|
|
|
|
def _get_offset_label_position_vectors(self, position_scalars: tuple[float, float, float]):
|
|
# Create position vectors
|
|
position_vectors = np.diag(position_scalars)
|
|
|
|
# Offset label positions radially by the tip radius
|
|
tip_radius = self._shaft_and_tip_geometry_source.tip_radius
|
|
offset_array = np.diag([tip_radius] * 3)
|
|
radial_offset1 = np.roll(offset_array, shift=1, axis=1)
|
|
radial_offset2 = np.roll(offset_array, shift=-1, axis=1)
|
|
|
|
position_vectors += radial_offset1 + radial_offset2
|
|
return position_vectors
|
|
|
|
def _update_label_positions(self):
|
|
labels = self._label_actors
|
|
position_vectors = self._get_offset_label_position_vectors(self.label_position)
|
|
for label, position in zip(labels, position_vectors):
|
|
label.relative_position = position
|
|
|
|
|
|
def _validate_label_sequence(labels: Sequence[str], n_labels: int | Sequence[int], name: str):
|
|
_validation.check_instance(labels, (list, tuple), name=name)
|
|
_validation.check_iterable_items(labels, str, name=name)
|
|
_validation.check_length(labels, exact_length=n_labels, name=name)
|
|
return labels
|
|
|
|
|
|
class AxesAssemblySymmetric(AxesAssembly):
|
|
"""Symmetric assembly of arrow-style axes parts.
|
|
|
|
This class is similar to :class:`~pyvista.AxesAssembly` but the axes are
|
|
symmetric.
|
|
|
|
The axes may be used as a widget or added to a scene.
|
|
|
|
Parameters
|
|
----------
|
|
x_label : str, default: ('+X', '-X')
|
|
Text labels for the positive and negative x-axis. Specify two strings or a
|
|
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
|
|
are added. Alternatively, set the labels with :attr:`labels`.
|
|
|
|
y_label : str, default: ('+Y', '-Y')
|
|
Text labels for the positive and negative y-axis. Specify two strings or a
|
|
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
|
|
are added. Alternatively, set the labels with :attr:`labels`.
|
|
|
|
z_label : str, default: ('+Z', '-Z')
|
|
Text labels for the positive and negative z-axis. Specify two strings or a
|
|
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
|
|
are added. Alternatively, set the labels with :attr:`labels`.
|
|
|
|
labels : Sequence[str], optional
|
|
Text labels for the axes. Specify three strings, one for each axis, or
|
|
six strings, one for each +/- axis. If three strings plus ``'+'`` and minus
|
|
``'-'`` characters are added. This is an alternative parameter to using
|
|
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
label_color : ColorLike, default: 'black'
|
|
Color of the text labels.
|
|
|
|
show_labels : bool, default: True
|
|
Show or hide the text labels.
|
|
|
|
label_position : float | VectorLike[float], optional
|
|
Position of the text labels along each axis. By default, the labels are
|
|
positioned at the ends of the shafts.
|
|
|
|
label_size : int, default: 50
|
|
Size of the text labels.
|
|
|
|
x_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the x-axis shaft and tip.
|
|
|
|
y_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the y-axis shaft and tip.
|
|
|
|
z_color : ColorLike | Sequence[ColorLike], optional
|
|
Color of the z-axis shaft and tip.
|
|
|
|
position : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Position of the axes in space.
|
|
|
|
orientation : VectorLike[float], default: (0, 0, 0)
|
|
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
|
|
around the y-axis first, then the x-axis, and finally the z-axis.
|
|
|
|
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Origin of the axes. This is the point about which all rotations take place. The
|
|
rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
|
|
|
|
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
|
|
Scaling factor applied to the axes.
|
|
|
|
user_matrix : MatrixLike[float], optional
|
|
A 4x4 transformation matrix applied to the axes. Defaults to the identity matrix.
|
|
The user matrix is the last transformation applied to the actor.
|
|
|
|
name : str, optional
|
|
The name of this assembly used when tracking on a plotter.
|
|
|
|
.. versionadded:: 0.45
|
|
|
|
**kwargs
|
|
Keyword arguments passed to :class:`pyvista.AxesGeometrySource`.
|
|
|
|
See Also
|
|
--------
|
|
AxesAssembly
|
|
|
|
:ref:`axes_objects_example`
|
|
Example showing different axes objects.
|
|
|
|
Examples
|
|
--------
|
|
Add symmetric axes to a plot.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric()
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes_assembly)
|
|
>>> pl.show()
|
|
|
|
Customize the axes labels.
|
|
|
|
>>> axes_assembly.labels = [
|
|
... 'east',
|
|
... 'west',
|
|
... 'north',
|
|
... 'south',
|
|
... 'up',
|
|
... 'down',
|
|
... ]
|
|
>>> axes_assembly.label_color = 'darkgoldenrod'
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(axes_assembly)
|
|
>>> pl.show()
|
|
|
|
Add the axes as a custom orientation widget with
|
|
:func:`~pyvista.Renderer.add_orientation_widget`. We also configure the labels to
|
|
only show text for the positive axes.
|
|
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric(
|
|
... x_label=('X', ''), y_label=('Y', ''), z_label=('Z', '')
|
|
... )
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_mesh(pv.Cone())
|
|
>>> _ = pl.add_orientation_widget(
|
|
... axes_assembly,
|
|
... viewport=(0, 0, 0.5, 0.5),
|
|
... )
|
|
>>> pl.show()
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
x_label: str | Sequence[str] | None = None,
|
|
y_label: str | Sequence[str] | None = None,
|
|
z_label: str | Sequence[str] | None = None,
|
|
labels: Sequence[str] | None = None,
|
|
label_color: ColorLike = 'black',
|
|
show_labels: bool = True,
|
|
label_position: float | VectorLike[float] | None = None,
|
|
label_size: int = 50,
|
|
x_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
y_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
z_color: ColorLike | Sequence[ColorLike] | None = None,
|
|
position: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
origin: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
|
|
user_matrix: MatrixLike[float] | None = None,
|
|
name: str | None = None,
|
|
**kwargs: Unpack[_AxesGeometryKwargs],
|
|
):
|
|
# Init shaft and tip actors
|
|
self._init_actors_from_source(AxesGeometrySource(symmetric=True, **kwargs))
|
|
# Init label actors
|
|
self._label_actors = (Label(), Label(), Label())
|
|
self._label_actors_symmetric = (Label(), Label(), Label())
|
|
|
|
_XYZAssembly.__init__(
|
|
self,
|
|
xyz_actors=tuple(zip(self._shaft_actors, self._tip_actors)), # type: ignore[arg-type]
|
|
xyz_label_actors=tuple(zip(self._label_actors, self._label_actors_symmetric)), # type: ignore[arg-type]
|
|
x_label=x_label,
|
|
y_label=y_label,
|
|
z_label=z_label,
|
|
labels=labels,
|
|
label_color=label_color,
|
|
show_labels=show_labels,
|
|
label_position=label_position,
|
|
label_size=label_size,
|
|
x_color=x_color,
|
|
y_color=y_color,
|
|
z_color=z_color,
|
|
position=position,
|
|
orientation=orientation,
|
|
origin=origin,
|
|
scale=scale,
|
|
user_matrix=user_matrix,
|
|
name=name,
|
|
)
|
|
self._set_default_label_props()
|
|
|
|
@property # type: ignore[override]
|
|
def labels(self) -> tuple[str, str, str, str, str, str]: # numpydoc ignore=RT01
|
|
"""Return or set the axes labels.
|
|
|
|
Specify three strings, one for each axis, or six strings, one for each +/- axis.
|
|
If three strings, plus ``'+'`` and minus ``'-'`` characters are added.
|
|
This property may be used as an alternative to using :attr:`x_label`,
|
|
:attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric()
|
|
|
|
Use three strings to set the labels. Plus ``'+'`` and minus ``'-'``
|
|
characters are added automatically.
|
|
|
|
>>> axes_assembly.labels = ['U', 'V', 'W']
|
|
>>> axes_assembly.labels
|
|
('+U', '-U', '+V', '-V', '+W', '-W')
|
|
|
|
Alternatively, use six strings to set the labels explicitly.
|
|
|
|
>>> axes_assembly.labels = [
|
|
... 'east',
|
|
... 'west',
|
|
... 'north',
|
|
... 'south',
|
|
... 'up',
|
|
... 'down',
|
|
... ]
|
|
>>> axes_assembly.labels
|
|
('east', 'west', 'north', 'south', 'up', 'down')
|
|
|
|
"""
|
|
return *self.x_label, *self.y_label, *self.z_label
|
|
|
|
@labels.setter
|
|
def labels(
|
|
self,
|
|
labels: list[str] | tuple[str, str, str] | tuple[str, str, str, str, str, str],
|
|
):
|
|
valid_labels = _validate_label_sequence(labels, n_labels=[3, 6], name='labels')
|
|
if len(valid_labels) == 3:
|
|
self.x_label = valid_labels[0]
|
|
self.y_label = valid_labels[1]
|
|
self.z_label = valid_labels[2]
|
|
else:
|
|
self.x_label = valid_labels[0:2]
|
|
self.y_label = valid_labels[2:4]
|
|
self.z_label = valid_labels[4:6]
|
|
|
|
def _get_axis_label(self, axis: _AxisEnum) -> tuple[str, str]:
|
|
label_plus = self._label_actors[axis].input
|
|
label_minus = self._label_actors_symmetric[axis].input
|
|
return label_plus, label_minus
|
|
|
|
def _set_axis_label(self, axis: _AxisEnum, label: str | list[str] | tuple[str, str]):
|
|
if isinstance(label, str):
|
|
label_plus, label_minus = '+' + label, '-' + label
|
|
else:
|
|
label_plus, label_minus = _validate_label_sequence(label, n_labels=2, name='label')
|
|
self._label_actors[axis].input = label_plus
|
|
self._label_actors_symmetric[axis].input = label_minus
|
|
|
|
@property # type: ignore[override]
|
|
def x_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
|
|
"""Return or set the labels for the positive and negative x-axis.
|
|
|
|
The labels may be set with a single string or two strings. If a single string,
|
|
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
|
|
labels with :attr:`labels`.
|
|
|
|
Examples
|
|
--------
|
|
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
|
|
characters are added automatically.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric()
|
|
>>> axes_assembly.x_label = 'Axis'
|
|
>>> axes_assembly.x_label
|
|
('+Axis', '-Axis')
|
|
|
|
Set the labels explicitly with two strings.
|
|
|
|
>>> axes_assembly.x_label = 'anterior', 'posterior'
|
|
>>> axes_assembly.x_label
|
|
('anterior', 'posterior')
|
|
|
|
"""
|
|
return self._get_axis_label(_AxisEnum.x)
|
|
|
|
@x_label.setter
|
|
def x_label(self, label: str | list[str] | tuple[str, str]):
|
|
self._set_axis_label(_AxisEnum.x, label)
|
|
|
|
@property # type: ignore[override]
|
|
def y_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
|
|
"""Return or set the labels for the positive and negative y-axis.
|
|
|
|
The labels may be set with a single string or two strings. If a single string,
|
|
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
|
|
labels with :attr:`labels`.
|
|
|
|
Examples
|
|
--------
|
|
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
|
|
characters are added automatically.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric()
|
|
>>> axes_assembly.y_label = 'Axis'
|
|
>>> axes_assembly.y_label
|
|
('+Axis', '-Axis')
|
|
|
|
Set the labels explicitly with two strings.
|
|
|
|
>>> axes_assembly.y_label = 'left', 'right'
|
|
>>> axes_assembly.y_label
|
|
('left', 'right')
|
|
|
|
"""
|
|
return self._get_axis_label(_AxisEnum.y)
|
|
|
|
@y_label.setter
|
|
def y_label(self, label: str | list[str] | tuple[str, str]):
|
|
self._set_axis_label(_AxisEnum.y, label)
|
|
|
|
@property # type: ignore[override]
|
|
def z_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
|
|
"""Return or set the labels for the positive and negative z-axis.
|
|
|
|
The labels may be set with a single string or two strings. If a single string,
|
|
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
|
|
labels with :attr:`labels`.
|
|
|
|
Examples
|
|
--------
|
|
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
|
|
characters are added automatically.
|
|
|
|
>>> import pyvista as pv
|
|
>>> axes_assembly = pv.AxesAssemblySymmetric()
|
|
>>> axes_assembly.z_label = 'Axis'
|
|
>>> axes_assembly.z_label
|
|
('+Axis', '-Axis')
|
|
|
|
Set the labels explicitly with two strings.
|
|
|
|
>>> axes_assembly.z_label = 'superior', 'inferior'
|
|
>>> axes_assembly.z_label
|
|
('superior', 'inferior')
|
|
|
|
"""
|
|
return self._get_axis_label(_AxisEnum.z)
|
|
|
|
@z_label.setter
|
|
def z_label(self, label: str | list[str] | tuple[str, str]):
|
|
self._set_axis_label(_AxisEnum.z, label)
|
|
|
|
def _update_label_positions(self):
|
|
# Update plus labels using parent method
|
|
AxesAssembly._update_label_positions(self)
|
|
|
|
# Update minus labels
|
|
label_position = self.label_position
|
|
label_position_minus = (
|
|
-label_position[0],
|
|
-label_position[1],
|
|
-label_position[2],
|
|
)
|
|
labels_minus = self._label_actors_symmetric
|
|
vector_position_minus = self._get_offset_label_position_vectors(label_position_minus)
|
|
for label, position in zip(labels_minus, vector_position_minus):
|
|
label.relative_position = position
|
|
|
|
|
|
class PlanesAssembly(_XYZAssembly):
|
|
"""Assembly of orthogonal planes.
|
|
|
|
Assembly of three orthogonal plane meshes with labels.
|
|
|
|
The labels can be 2D or 3D, and will follow the camera such that they have the
|
|
correct orientation and remain parallel to the edges of the planes.
|
|
|
|
The positioning of the labels may be customized using the :attr:`label_edge`,
|
|
:attr:`label_position`, and :attr:`label_offset` attributes.
|
|
|
|
.. warning::
|
|
|
|
The :attr:`camera` must be set before rendering the assembly. Otherwise,
|
|
attempting to render it will cause python to crash.
|
|
|
|
.. versionadded:: 0.45
|
|
|
|
Parameters
|
|
----------
|
|
x_label : str, default: 'YZ'
|
|
Text label for the yz-plane. Alternatively, set the label with :attr:`labels`.
|
|
|
|
y_label : str, default: 'ZX'
|
|
Text label for the zx-plane. Alternatively, set the label with :attr:`labels`.
|
|
|
|
z_label : str, default: 'XY'
|
|
Text label for the xy-plane. Alternatively, set the label with :attr:`labels`.
|
|
|
|
labels : Sequence[str], optional,
|
|
Text labels for the planes. This is an alternative parameter to using
|
|
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
label_color : ColorLike, default: 'black'
|
|
Color of the text labels.
|
|
|
|
show_labels : bool, default: True
|
|
Show or hide the text labels.
|
|
|
|
label_position : float | VectorLike[float], default: 0.5
|
|
Normalized relative position of the text labels along each plane's respective
|
|
:attr:`label_edge`. The positions are normalized to have a range of
|
|
``[-1.0, 1.0]`` such that ``0.0`` is at the center of the edge and ``-1.0`` and
|
|
``1.0`` are at the corners.
|
|
|
|
.. note::
|
|
|
|
The label text is centered horizontally at the specified positions.
|
|
|
|
label_edge : str, default: 'right'
|
|
Edge on which to position each plane's label. Can be ``'top'``, ``'bottom'``,
|
|
``'right'``, or ``'left'``. Use a single value to set the edge for all labels
|
|
or set each edge independently.
|
|
|
|
label_offset : float | VectorLike[float], optional
|
|
Vertical offset of the text labels. The offset is proportional to
|
|
the :attr:`~pyvista.Prop3D.length` of the assembly. Positive values move the labels away
|
|
from the center; negative values move them towards it.
|
|
|
|
label_size : int, default: 50
|
|
Size of the text labels. If :attr:`label_mode` is ``'2D'``, this is the
|
|
font size. If :attr:`label_mode` is ``'3D'``, the labels are scaled
|
|
proportional to the :attr:`~pyvista.Prop3D.length` of the assembly.
|
|
|
|
label_mode : '2D' | '3D', default: '2D'
|
|
Mode to use for text labels. In 2D mode, the label actors are always visible
|
|
and have a constant size regardless of window size. In 3D mode, the label actors
|
|
may be occluded by other geometry and will scale with changes to the window
|
|
size. The two modes also have minor differences in appearance and behavior in
|
|
terms of how they follow the camera.
|
|
|
|
x_color : ColorLike, optional
|
|
Color of the xy-plane.
|
|
|
|
y_color : ColorLike, optional
|
|
Color of the yz-plane.
|
|
|
|
z_color : ColorLike, optional
|
|
Color of the zx-plane.
|
|
|
|
opacity : float, default: 0.3
|
|
Opacity of the planes.
|
|
|
|
position : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Position of the planes in space.
|
|
|
|
orientation : VectorLike[float], default: (0, 0, 0)
|
|
Orientation angles of the assembly 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
|
|
around the y-axis first, then the x-axis, and finally the z-axis.
|
|
|
|
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
|
|
Origin of the assembly. This is the point about which all rotations take place.
|
|
The rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
|
|
|
|
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
|
|
Scaling factor applied to the assembly.
|
|
|
|
user_matrix : MatrixLike[float], optional
|
|
A 4x4 transformation matrix applied to the assembly. Defaults to the identity
|
|
matrix. The user matrix is the last transformation applied to the actor.
|
|
|
|
name : str, optional
|
|
The name of this assembly used when tracking on a plotter.
|
|
|
|
.. versionadded:: 0.45
|
|
|
|
**kwargs
|
|
Keyword arguments passed to :class:`pyvista.OrthogonalPlanesSource`.
|
|
|
|
Examples
|
|
--------
|
|
Fit planes to a model of a human.
|
|
|
|
>>> import numpy as np
|
|
>>> import pyvista as pv
|
|
>>> from pyvista import examples
|
|
>>> human = examples.download_human()
|
|
>>> labels = 'Sagittal', 'Coronal', 'Transverse'
|
|
>>> planes = pv.PlanesAssembly(
|
|
... bounds=human.bounds, labels=labels, label_size=25
|
|
... )
|
|
|
|
Plot the planes and the model.
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> human_actor = pl.add_mesh(human, scalars='Color', rgb=True)
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Apply a transformation to the planes and the model.
|
|
|
|
>>> transform = np.array(
|
|
... [
|
|
... [0.70645893, 0.69636424, 0.12646197, 1.0],
|
|
... [-0.62246712, 0.69636424, -0.35722756, 2.0],
|
|
... [-0.33682409, 0.17364818, 0.92541658, 3.0],
|
|
... [0.0, 0.0, 0.0, 1.0],
|
|
... ]
|
|
... )
|
|
>>> planes.user_matrix = transform
|
|
>>> human_actor.user_matrix = transform
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(human_actor)
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Create a new assembly and customize the colors and opacity.
|
|
|
|
>>> color = pv.global_theme.color
|
|
>>> planes = pv.PlanesAssembly(
|
|
... bounds=human.bounds,
|
|
... x_color=color,
|
|
... y_color=color,
|
|
... z_color=color,
|
|
... opacity=1.0,
|
|
... )
|
|
|
|
Since the planes are opaque, the 3D labels may be occluded. Use 2D labels instead
|
|
so the labels are always visible.
|
|
|
|
>>> planes.label_mode = '2D'
|
|
|
|
Offset the labels to position them inside the bounds of the planes.
|
|
|
|
>>> planes.label_offset = -0.05
|
|
|
|
Move the labels for the two larger planes closer to the corners.
|
|
|
|
>>> planes.label_position = (0.8, 0.8, 0.5)
|
|
|
|
Visualize the result.
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_mesh(human, scalars='Color', rgb=True)
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
"""
|
|
|
|
DEFAULT_LABELS = _XYZTuple('YZ', 'ZX', 'XY')
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
x_label: str | None = None,
|
|
y_label: str | None = None,
|
|
z_label: str | None = None,
|
|
labels: Sequence[str] | None = None,
|
|
label_color: ColorLike = 'black',
|
|
show_labels: bool = True,
|
|
label_position: float | VectorLike[float] = 0.5,
|
|
label_edge: Literal['top', 'bottom', 'right', 'left'] | Sequence[str] = 'right',
|
|
label_offset: float = 0.05,
|
|
label_size: int = 50,
|
|
label_mode: Literal['2D', '3D'] = '3D',
|
|
x_color: ColorLike | None = None,
|
|
y_color: ColorLike | None = None,
|
|
z_color: ColorLike | None = None,
|
|
opacity: float | VectorLike[float] = 0.3,
|
|
position: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
origin: VectorLike[float] = (0.0, 0.0, 0.0),
|
|
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
|
|
user_matrix: MatrixLike[float] | None = None,
|
|
name: str | None = None,
|
|
**kwargs: Unpack[_OrthogonalPlanesKwargs],
|
|
):
|
|
self._camera = None
|
|
|
|
# Init plane actors
|
|
self._plane_actors = (Actor(), Actor(), Actor())
|
|
# Init planes from source
|
|
self._geometry_source = OrthogonalPlanesSource(**kwargs)
|
|
self._planes = self._geometry_source.output
|
|
self._plane_sources = self._geometry_source.sources
|
|
|
|
for actor, dataset in zip(self._plane_actors, self.planes):
|
|
actor.mapper = pv.DataSetMapper(dataset=dataset)
|
|
|
|
# Init label actors
|
|
self._axis_actors = (_AxisActor(), _AxisActor(), _AxisActor())
|
|
|
|
# Tempt init values for call to super class, will validate inputs later
|
|
self._label_offset = 0.05
|
|
self._label_edge = ('right', 'right', 'right')
|
|
self._label_position = 0.5, 0.5, 0.5
|
|
|
|
_XYZAssembly.__init__(
|
|
self,
|
|
xyz_actors=self._plane_actors,
|
|
xyz_label_actors=self._axis_actors,
|
|
x_label=x_label,
|
|
y_label=y_label,
|
|
z_label=z_label,
|
|
labels=labels,
|
|
label_color=label_color,
|
|
show_labels=show_labels,
|
|
label_position=label_position,
|
|
label_size=label_size,
|
|
x_color=x_color,
|
|
y_color=y_color,
|
|
z_color=z_color,
|
|
position=position,
|
|
orientation=orientation,
|
|
origin=origin,
|
|
scale=scale,
|
|
user_matrix=user_matrix,
|
|
name=name,
|
|
)
|
|
|
|
self.opacity = opacity # type: ignore[assignment]
|
|
self.label_mode = label_mode
|
|
self.label_offset = label_offset
|
|
self.label_edge = label_edge
|
|
|
|
# Set default properties
|
|
for actor in self._plane_actors:
|
|
prop = actor.prop
|
|
prop.show_edges = True
|
|
prop.line_width = 3
|
|
|
|
# Set default text properties
|
|
# TODO: implement set_text_prop() and use that instead
|
|
for label in self._label_actor_iterator:
|
|
prop = label.prop
|
|
prop.justification_vertical = 'center'
|
|
prop.justification_horizontal = 'center'
|
|
|
|
def __repr__(self):
|
|
"""Representation of the planes assembly."""
|
|
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
|
|
bnds = self.bounds
|
|
|
|
attr = [
|
|
f'{type(self).__name__} ({hex(id(self))})',
|
|
f' Resolution: {self._geometry_source.resolution}',
|
|
f' Normal sign: {self._geometry_source.normal_sign}',
|
|
f" X label: '{self.x_label}'",
|
|
f" Y label: '{self.y_label}'",
|
|
f" Z label: '{self.z_label}'",
|
|
f' Label color: {self.label_color}',
|
|
f' Show labels: {self.show_labels}',
|
|
f' Label position: {self.label_position}',
|
|
f' Label edge: {self.label_edge}',
|
|
f' Label offset: {self.label_offset}',
|
|
f" Label mode: '{self.label_mode}'",
|
|
f' X Color: {self.x_color}',
|
|
f' Y Color: {self.y_color}',
|
|
f' Z Color: {self.z_color}',
|
|
f' Position: {self.position}',
|
|
f' Orientation: {self.orientation}',
|
|
f' Origin: {self.origin}',
|
|
f' Scale: {self.scale}',
|
|
f' User matrix: {mat_info}',
|
|
f' X Bounds {bnds.x_min:.3E}, {bnds.x_max:.3E}',
|
|
f' Y Bounds {bnds.y_min:.3E}, {bnds.y_max:.3E}',
|
|
f' Z Bounds {bnds.z_min:.3E}, {bnds.z_max:.3E}',
|
|
]
|
|
return '\n'.join(attr)
|
|
|
|
@property
|
|
def labels(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
|
|
"""Return or set the labels for the planes.
|
|
|
|
This property may be used as an alternative to using :attr:`x_label`,
|
|
:attr:`y_label`, and :attr:`z_label` separately.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly()
|
|
>>> planes.labels = ['Sagittal', 'Coronal', 'Transverse']
|
|
>>> planes.labels
|
|
('Sagittal', 'Coronal', 'Transverse')
|
|
|
|
"""
|
|
return self.x_label, self.y_label, self.z_label
|
|
|
|
@labels.setter
|
|
def labels(self, labels: list[str] | tuple[str, str, str]):
|
|
labels = _validate_label_sequence(labels, n_labels=3, name='labels')
|
|
self.x_label = labels[0]
|
|
self.y_label = labels[1]
|
|
self.z_label = labels[2]
|
|
|
|
@property
|
|
def x_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the yz-plane.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly()
|
|
>>> planes.x_label = 'This plane'
|
|
>>> planes.x_label
|
|
'This plane'
|
|
|
|
"""
|
|
return self._axis_actors[0].GetTitle()
|
|
|
|
@x_label.setter
|
|
def x_label(self, label: str):
|
|
self._axis_actors[0].SetTitle(label)
|
|
self.planes.set_block_name(0, label)
|
|
|
|
@property
|
|
def y_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the zx-plane.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly()
|
|
>>> planes.y_label = 'This plane'
|
|
>>> planes.y_label
|
|
'This plane'
|
|
|
|
"""
|
|
return self._axis_actors[1].GetTitle()
|
|
|
|
@y_label.setter
|
|
def y_label(self, label: str):
|
|
self._axis_actors[1].SetTitle(label)
|
|
self.planes.set_block_name(1, label)
|
|
|
|
@property
|
|
def z_label(self) -> str: # numpydoc ignore=RT01
|
|
"""Text label for the xy-plane.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly()
|
|
>>> planes.z_label = 'This plane'
|
|
>>> planes.z_label
|
|
'This plane'
|
|
|
|
"""
|
|
return self._axis_actors[2].GetTitle()
|
|
|
|
@z_label.setter
|
|
def z_label(self, label: str):
|
|
self._axis_actors[2].SetTitle(label)
|
|
self.planes.set_block_name(2, label)
|
|
|
|
@property
|
|
def label_size(self) -> int: # numpydoc ignore=RT01
|
|
"""Size of the text labels.
|
|
|
|
Must be a positive integer.
|
|
"""
|
|
return self._label_size
|
|
|
|
@label_size.setter
|
|
def label_size(self, size: int):
|
|
valid_size = _validation.validate_number(
|
|
size,
|
|
must_be_in_range=[0, np.inf],
|
|
must_be_integer=True,
|
|
dtype_out=int,
|
|
name='label size',
|
|
)
|
|
self._label_size = valid_size
|
|
# 2D labels use font size (int) but 3D labels use a scaling factor (float)
|
|
# For 3D labels, we re-scale the text proportional to the planes assembly
|
|
# Values on the order of 0.01-0.05 seem to work best. Use a normalization
|
|
# factor so that input values are on the order of 10-50 and roughly match 2D sizes
|
|
NORM_FACTOR = 1000
|
|
scale_3d = self.planes.length * float(valid_size) / NORM_FACTOR
|
|
|
|
# In VTK 9.6+, the 3D label size depends on the 2D label size, so in the 3D case
|
|
# we need to reset the 2D font size to match the VTK default value of 12
|
|
font_size_2d = (
|
|
valid_size if hasattr(self, 'label_mode') and self.label_mode == '2D' else 12
|
|
)
|
|
for axis in self._axis_actors:
|
|
axis.GetTitleActor().SetScale(scale_3d) # 3D labels
|
|
axis.GetTitleTextProperty().SetFontSize(font_size_2d) # 2D labels
|
|
|
|
@property
|
|
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
|
|
"""Normalized relative position of the text labels along the edge of each plane.
|
|
|
|
Labels are positioned relative to each plane's respective :attr:`label_edge`.
|
|
The positions are normalized to have a range of ``[-1.0, 1.0]`` such that ``0.0``
|
|
is at the center of the edge and ``-1.0`` and ``1.0`` are at the corners.
|
|
|
|
.. note::
|
|
|
|
The label text is centered horizontally at the specified positions.
|
|
|
|
Examples
|
|
--------
|
|
Position the labels at the center (along the edges) and plot the assembly.
|
|
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly(label_position=0)
|
|
>>> planes.label_position
|
|
(0.0, 0.0, 0.0)
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Position the labels at the corners.
|
|
|
|
>>> planes.label_position = 1.0
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Vary the position of the labels independently for each plane. The values may be
|
|
negative and/or exceed a value of ``1.0``.
|
|
|
|
>>> planes.label_position = (-1.3, -1.0, -0.5)
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
"""
|
|
return self._label_position
|
|
|
|
@label_position.setter
|
|
def label_position(self, position: int | VectorLike[int]):
|
|
self._label_position = _validation.validate_array3(
|
|
position,
|
|
broadcast=True,
|
|
name='Label position',
|
|
dtype_out=float,
|
|
to_tuple=True,
|
|
)
|
|
self._update_label_positions()
|
|
|
|
@property
|
|
def label_edge(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
|
|
"""Edge on which to position each plane's label.
|
|
|
|
Edge can be ``'top'``,``'bottom'``,``'right'``, or ``'left'``, and can be
|
|
set independently for each plane or to the same edge for all planes.
|
|
|
|
The edge is relative to each plane's local ``i`` and ``j`` coordinates.
|
|
|
|
Examples
|
|
--------
|
|
Position the labels at the top edge and plot.
|
|
|
|
>>> import pyvista as pv
|
|
>>> planes = pv.PlanesAssembly(label_edge='top')
|
|
>>> planes.label_edge
|
|
('top', 'top', 'top')
|
|
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Position the labels at the bottom.
|
|
|
|
>>> planes.label_edge = 'bottom'
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
Vary the edge of the labels independently for each plane.
|
|
|
|
>>> planes.label_edge = ('top', 'right', 'left')
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(planes)
|
|
>>> planes.camera = pl.camera
|
|
>>> pl.show()
|
|
|
|
"""
|
|
return self._label_edge
|
|
|
|
@label_edge.setter
|
|
def label_edge(self, edge: Literal['top', 'bottom', 'right', 'left'] | Sequence[str]):
|
|
valid_edge = (
|
|
[edge] * 3
|
|
if isinstance(edge, str)
|
|
else _validate_label_sequence(edge, n_labels=3, name='label edge')
|
|
)
|
|
for edge_ in valid_edge:
|
|
_validation.check_contains(
|
|
['top', 'bottom', 'right', 'left'],
|
|
must_contain=edge_,
|
|
name='label_edge',
|
|
)
|
|
self._label_edge = tuple(valid_edge)
|
|
self._update_label_positions()
|
|
|
|
@property
|
|
def label_offset(self) -> float: # numpydoc ignore=RT01
|
|
"""Vertical offset of the text labels.
|
|
|
|
The offset is proportional to the :attr:`~pyvista.Prop3D.length` of the assembly. Positive
|
|
values move the labels away from the center; negative values move them
|
|
towards it.
|
|
"""
|
|
return self._label_offset
|
|
|
|
@label_offset.setter
|
|
def label_offset(self, offset: float):
|
|
self._label_offset = _validation.validate_number(offset, dtype_out=float)
|
|
self._update_label_positions()
|
|
|
|
@property
|
|
def label_mode(self) -> Literal['2D', '3D']: # numpydoc ignore=RT01
|
|
"""Mode to use for text labels.
|
|
|
|
Mode must be either ``'2D'`` or ``'3D'``. In 2D mode, the label actors are
|
|
always visible and have a constant size regardless of window size. In 3D mode,
|
|
the label actors may be occluded by other geometry and will scale with changes
|
|
to the window size. The two modes also have minor differences in appearance as
|
|
well as behavior in terms of how they follow the camera.
|
|
"""
|
|
return self._label_mode
|
|
|
|
@label_mode.setter
|
|
def label_mode(self, mode: Literal['2D', '3D']):
|
|
_validation.check_contains(['2D', '3D'], must_contain=mode, name='label_mode')
|
|
self._label_mode = mode
|
|
use_2D = mode == '2D'
|
|
for axis in self._axis_actors:
|
|
axis.SetUse2DMode(use_2D)
|
|
|
|
# The 3D label size depends on the 2D label size so we need to reset this property
|
|
self.label_size = self.label_size
|
|
|
|
@property
|
|
def x_color(self) -> Color: # numpydoc ignore=RT01
|
|
"""Color of the yz-plane."""
|
|
return self._plane_actors[0].prop.color
|
|
|
|
@x_color.setter
|
|
def x_color(self, color: ColorLike):
|
|
self._plane_actors[0].prop.color = color
|
|
|
|
@property
|
|
def y_color(self) -> Color: # numpydoc ignore=RT01
|
|
"""Color of the zx-plane."""
|
|
return self._plane_actors[1].prop.color
|
|
|
|
@y_color.setter
|
|
def y_color(self, color: ColorLike):
|
|
self._plane_actors[1].prop.color = color
|
|
|
|
@property
|
|
def z_color(self) -> Color: # numpydoc ignore=RT01
|
|
"""Color of the xy-plane."""
|
|
return self._plane_actors[2].prop.color
|
|
|
|
@z_color.setter
|
|
def z_color(self, color: ColorLike):
|
|
self._plane_actors[2].prop.color = color
|
|
|
|
@property
|
|
def opacity(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
|
|
"""Opacity of the planes."""
|
|
return self._opacity
|
|
|
|
@opacity.setter
|
|
def opacity(self, opacity: float):
|
|
valid_opacity = _validation.validate_array3(
|
|
opacity, broadcast=True, dtype_out=float, to_tuple=True
|
|
)
|
|
self._opacity = valid_opacity
|
|
for actor, opacity_ in zip(self._plane_actors, valid_opacity):
|
|
actor.prop.opacity = opacity_
|
|
|
|
@property
|
|
def camera(self): # numpydoc ignore=RT01
|
|
"""Camera to use for displaying the labels."""
|
|
return self._camera
|
|
|
|
@camera.setter
|
|
def camera(self, camera):
|
|
self._camera = camera
|
|
for axis in self._axis_actors:
|
|
axis.SetCamera(camera)
|
|
|
|
@property
|
|
def planes(self):
|
|
"""Get the orthogonal plane datasets of the assembly.
|
|
|
|
The planes are :class:`pyvista.PolyData` meshes stored as a
|
|
:class:`pyvista.MultiBlock`. The names of the blocks match the names of the
|
|
assembly's :attr:`labels`.
|
|
|
|
The planes are initially generated with :class:`pyvista.OrthogonalPlanesSource`.
|
|
|
|
Returns
|
|
-------
|
|
pyvista.MultiBlock
|
|
Composite mesh with three planes.
|
|
|
|
"""
|
|
return self._planes
|
|
|
|
def _update_label_positions(self):
|
|
axis_actors = self._axis_actors
|
|
plane_sources = self._plane_sources
|
|
transformation_matrix = self._transformation_matrix
|
|
|
|
def transform_point(point):
|
|
return (transformation_matrix @ (*point, 1))[:3]
|
|
|
|
def set_axis_location(plane_id, edge: str, position: float):
|
|
this_plane_source = plane_sources[plane_id]
|
|
this_axis_actor = axis_actors[plane_id]
|
|
|
|
# Get vectors which define the plane
|
|
origin, point1, point2 = (
|
|
np.array(this_plane_source.GetOrigin()),
|
|
np.array(this_plane_source.GetPoint1()),
|
|
np.array(this_plane_source.GetPoint2()),
|
|
)
|
|
|
|
vector1 = point1 - origin
|
|
vector2 = point2 - origin
|
|
|
|
# Define corners
|
|
corner_bottom_left = origin
|
|
corner_bottom_right = origin + vector1
|
|
corner_top_left = origin + vector2
|
|
corner_top_right = corner_bottom_right + vector2
|
|
|
|
# Define axis points in counter-clockwise order
|
|
if edge == 'top':
|
|
axis_point1, axis_point2 = corner_top_right, corner_top_left
|
|
elif edge == 'left':
|
|
axis_point1, axis_point2 = corner_top_left, corner_bottom_left
|
|
elif edge == 'bottom':
|
|
axis_point1, axis_point2 = corner_bottom_left, corner_bottom_right
|
|
else: # 'right'
|
|
axis_point1, axis_point2 = corner_bottom_right, corner_top_right
|
|
|
|
# Move axis to position along the edge
|
|
axis_vector = axis_point1 - axis_point2
|
|
# Define position relative to center of edge
|
|
position_vector = np.abs(axis_vector) * position * 0.5
|
|
axis_point1 += position_vector
|
|
axis_point2 += position_vector
|
|
|
|
# Add offset
|
|
axis_dir = axis_vector / np.linalg.norm(axis_vector)
|
|
offset_dir = np.cross(axis_dir, this_plane_source.GetNormal())
|
|
offset_dir = -1 * offset_dir / np.linalg.norm(offset_dir)
|
|
offset_mag = self.planes.length * self.label_offset
|
|
offset = offset_mag * offset_dir
|
|
axis_point1 += offset
|
|
axis_point2 += offset
|
|
|
|
# Set axis points
|
|
this_axis_actor.SetPoint1(transform_point(axis_point1))
|
|
this_axis_actor.SetPoint2(transform_point(axis_point2))
|
|
|
|
edge = self.label_edge
|
|
position = self.label_position
|
|
set_axis_location(0, edge[0], position[0])
|
|
set_axis_location(1, edge[1], position[1])
|
|
set_axis_location(2, edge[2], position[2])
|
|
|
|
def _post_set_update(self):
|
|
_XYZAssembly._post_set_update(self)
|
|
# Need to manually update axis actors
|
|
self._update_label_positions()
|
|
|
|
|
|
class _AxisActor(_vtk.DisableVtkSnakeCase, _vtk.vtkAxisActor):
|
|
def __init__(self):
|
|
super().__init__()
|
|
# Only show the title
|
|
self.TitleVisibilityOn()
|
|
self.MinorTicksVisibleOff()
|
|
self.TickVisibilityOff()
|
|
self.DrawGridlinesOff()
|
|
self.AxisVisibilityOff() # Turn this on for debugging
|
|
|
|
# Set empty tick labels
|
|
labels = _vtk.vtkStringArray()
|
|
labels.SetNumberOfTuples(0)
|
|
# labels.SetValue(0, "")
|
|
self.SetLabels(labels)
|
|
|
|
# Ignore the axis bounds when rendering. Otherwise, the bounds must be
|
|
# set with SetBounds() every time the axis is updated
|
|
self.SetUseBounds(False)
|
|
|
|
# Format title positioning
|
|
offset = (0,) if pv.vtk_version_info < (9, 3) else (0, 0)
|
|
self.SetTitleOffset(*offset)
|
|
self.SetLabelOffset(0)
|
|
|
|
# For 2D mode only
|
|
self.SetVerticalOffsetXTitle2D(0)
|
|
self.SetHorizontalOffsetYTitle2D(0)
|
|
text_prop = TextProperty()
|
|
text_prop.justification_vertical = 'center'
|
|
self.SetTitleTextProperty(text_prop)
|
|
self.GetTitleActor()
|
|
|
|
# For 3D mode only
|
|
self.GetProperty().SetLighting(False)
|
|
|
|
@property
|
|
def prop(self) -> TextProperty:
|
|
return self.GetTitleTextProperty()
|