Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/466.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feat: more customization APIs
30 changes: 27 additions & 3 deletions examples/00-basic-pyvista-examples/customization_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,36 @@


# Scene title at the top center
plotter.add_text("Customization API Example", position=(0.5, 0.95), font_size=18, color='white')
plotter.add_text("Customization API Example", position="upper_edge", font_size=18, color='black')

# Additional labels at the top corners
plotter.add_text("Plotly Backend", position=(0.05, 0.95), font_size=12, color='lightblue')
# Additional labels at the top left corner using a string for the position as before
plotter.add_text("PyVista Backend", position="upper_left", font_size=12, color='lightblue')
# Additional labels at the bottom left corner using pixel coordinates
plotter.add_text("3D Visualization", position=(0.95, 0.95), font_size=12, color='lightgreen')


# Add labels at specific 3D points to annotate key locations in space.

label_points = [
[1, 0, 0], # X axis endpoint
[0, 1, 0], # Y axis endpoint
[0, 0, 1], # Z axis endpoint
]

labels = ['X-axis', 'Y-axis', 'Z-axis']

plotter.add_labels(label_points, labels, font_size=16, point_size=8.0)


# The clear() method resets the plotter and can be called even after show().
# This allows reusing the same plotter for multiple visualizations.

# Uncomment to clear everything added above and start fresh:
# plotter.show()
# plotter.clear()
# plotter.plot(pv.Cube()) # Would show only a cube instead
Comment thread
RobPasMue marked this conversation as resolved.


# Display the visualization with all customizations.

plotter.show()
24 changes: 23 additions & 1 deletion examples/01-basic-plotly-examples/customization_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,35 @@
# Add text annotations using 2D normalized coordinates (0-1 range).

# Scene title at the top center
plotter.add_text("Customization API Example", position=(0.5, 0.95), font_size=18, color='white')
plotter.add_text("Customization API Example", position=(0.5, 0.95), font_size=18, color='black')

# Additional labels at the top corners
plotter.add_text("Plotly Backend", position=(0.05, 0.95), font_size=12, color='lightblue')
plotter.add_text("3D Visualization", position=(0.95, 0.95), font_size=12, color='lightgreen')


# Add labels at specific 3D points to annotate key locations in space.

label_points = [
[1, 0, 0], # X axis endpoint
[0, 1, 0], # Y axis endpoint
[0, 0, 1], # Z axis endpoint
]

labels = ['X-axis', 'Y-axis', 'Z-axis']

plotter.add_labels(label_points, labels, font_size=16, point_size=8.0)


# The clear() method resets the plotter and can be called even after show().
# This allows reusing the same plotter for multiple visualizations.

# Uncomment to clear everything added above and start fresh:
# plotter.show()
# plotter.clear()
# plotter.plot(pv.Cube()) # Would show only a cube instead
Comment thread
RobPasMue marked this conversation as resolved.


# Display the visualization with all customizations.


Expand Down
51 changes: 51 additions & 0 deletions src/ansys/tools/visualization_interface/backends/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,54 @@ def add_text(
Backend-specific actor or object representing the added text.
"""
raise NotImplementedError("add_text method must be implemented")

@abstractmethod
def add_labels(
self,
points: Union[List, Any],
labels: List[str],
font_size: int = 12,
point_size: float = 5.0,
**kwargs
) -> Any:
"""Add labels at 3D point locations.

Parameters
----------
points : Union[List, Any]
Points where labels should be placed. Can be a list of coordinates
or array-like object. Expected format: [[x1, y1, z1], ...] or Nx3 array.
labels : List[str]
List of label strings to display at each point.
font_size : int, default: 12
Font size for the labels.
point_size : float, default: 5.0
Size of the point markers shown with labels.
**kwargs : dict
Additional backend-specific keyword arguments.

Returns
-------
Any
Backend-specific actor or object representing the added labels.
"""
raise NotImplementedError("add_labels method must be implemented")

@abstractmethod
def clear(self) -> None:
"""Clear all actors from the scene.

This method removes all previously added objects (meshes, points, lines,
text, etc.) from the visualization scene, therefore allowing
the plotter to be reused after ``show()`` has been called.

Notes
-----
Both PyVista and Plotly backends support clearing and reusing the
plotter after ``show()`` has been called. This enables workflows like:

1. Add objects and call ``show()``
2. Call ``clear()`` to reset
3. Add new objects and call ``show()`` again
"""
raise NotImplementedError("clear method must be implemented")
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,55 @@ def add_text(
)
self._fig.add_annotation(annotation)
return annotation

def add_labels(
self,
points: Union[List, Any],
labels: List[str],
font_size: int = 12,
point_size: float = 5.0,
**kwargs
) -> Any:
"""Add labels at 3D point locations.

Parameters
----------
points : Union[List, Any]
Points where labels should be placed.
labels : List[str]
List of label strings to display at each point.
font_size : int, default: 12
Font size for the labels.
point_size : float, default: 5.0
Size of the point markers shown with labels.
**kwargs : dict
Additional keyword arguments.

Returns
-------
Any
Plotly trace representing the labels.
"""
import numpy as np

points_array = np.asarray(points)
if points_array.ndim == 1:
points_array = points_array.reshape(-1, 3)

# Create a scatter trace with both markers and text
trace = go.Scatter3d(
x=points_array[:, 0],
y=points_array[:, 1],
z=points_array[:, 2],
mode='markers+text',
text=labels,
textfont=dict(size=font_size),
marker=dict(size=point_size),
**kwargs
)
self._fig.add_trace(trace)
return trace

def clear(self) -> None:
"""Clear all traces from the figure."""
self._fig.data = []
139 changes: 110 additions & 29 deletions src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
# SOFTWARE.
"""Provides a wrapper to aid in plotting."""
from abc import abstractmethod
from collections.abc import Callable
import importlib.util
from typing import Any, Dict, List, Optional, Union

Expand Down Expand Up @@ -58,6 +57,10 @@
)
from ansys.tools.visualization_interface.backends.pyvista.widgets.widget import PlotterWidget
from ansys.tools.visualization_interface.types.edge_plot import EdgePlot
from ansys.tools.visualization_interface.utils._kwargs_manager import (
_capture_init_params,
_extract_kwargs,
)
from ansys.tools.visualization_interface.utils.color import Color
from ansys.tools.visualization_interface.utils.logger import logger

Expand Down Expand Up @@ -122,6 +125,9 @@ def __init__(
from vtkmodules.vtkInteractionWidgets import vtkHoverWidget
from vtkmodules.vtkRenderingCore import vtkPointPicker

# Save initialization parameters for potential reinitialization via clear()
self._init_params = _capture_init_params(self.__init__, locals())

# Check if the use of trame was requested
if use_trame is None:
use_trame = ansys.tools.visualization_interface.USE_TRAME
Expand Down Expand Up @@ -184,6 +190,40 @@ def __init__(
else:
raise TypeError("custom_picker must be an instance of AbstractPicker.")

def _cleanup(self) -> None:
"""Clean up resources before reinitialization.

This method releases VTK resources, disables widgets and observers,
and closes the existing plotter.
"""
# Disable hover widget if active
if self._hover_widget is not None:
try:
self._hover_widget.EnabledOff()
except Exception:
logger.warning("Failed to disable hover widget. It may not be active.")

# Disable picking if it was enabled
if self._allow_picking and self._pl is not None:
try:
self._pl.scene.disable_picking()
except Exception:
logger.warning("Failed to disable picking. It may not be active.")

# Clear widgets list
self._widgets.clear()

# Clear object-to-actor maps
self._object_to_actors_map.clear()
self._edge_actors_map.clear()

# Close the existing PyVista plotter to release VTK resources
if self._pl is not None:
try:
self._pl.scene.close()
except Exception:
logger.warning("Failed to close the plotter. It may already be closed.")

@property
def pv_interface(self) -> PyVistaInterface:
"""PyVista interface."""
Expand Down Expand Up @@ -370,31 +410,6 @@ def disable_center_focus(self):
self._pl.scene.disable_picking()
self._picked_ball.SetVisibility(False)

def __extract_kwargs(self, func_name: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""Extracts the keyword arguments from a function signature and returns it as dict.

Parameters
----------
func_name : Callable
Function to extract the keyword arguments from. It should be a callable function
input_kwargs : Dict[str, Any]
Dictionary with the keyword arguments to update the extracted ones.

Returns
-------
Dict[str, Any]
Dictionary with the keyword arguments extracted from the function signature and
updated with the input kwargs.
"""
import inspect
signature = inspect.signature(func_name)
kwargs = {}
for k, v in signature.parameters.items():
# We are ignoring positional arguments, and passing everything as kwarg
if v.default is not inspect.Parameter.empty:
kwargs[k] = input_kwargs[k] if k in input_kwargs else v.default
return kwargs

def show(
self,
plottable_object: Any = None,
Expand Down Expand Up @@ -430,11 +445,11 @@ def show(
List with the picked bodies in the picked order.

"""
plotting_options = self.__extract_kwargs(
plotting_options = _extract_kwargs(
self._pl._scene.add_mesh,
kwargs,
)
show_options = self.__extract_kwargs(
show_options = _extract_kwargs(
self._pl.scene.show,
kwargs,
)
Expand Down Expand Up @@ -599,6 +614,9 @@ def __init__(
**plotter_kwargs,
)

# Save initialization parameters for reinitialization via clear()
self._init_params = _capture_init_params(self.__init__, locals())

@property
def base_plotter(self):
"""Return the base plotter object."""
Expand Down Expand Up @@ -785,7 +803,6 @@ def add_points(
point_cloud,
color=color,
point_size=size,
render_points_as_spheres=True,
**kwargs
)

Expand Down Expand Up @@ -952,3 +969,67 @@ def add_text(
)

return actor

def add_labels(
self,
points: Union[List, Any],
labels: List[str],
font_size: int = 12,
point_size: float = 5.0,
**kwargs
) -> "pv.Actor":
"""Add labels at 3D point locations.

Parameters
----------
points : Union[List, Any]
Points where labels should be placed. Can be a list of coordinates
or array-like object. Expected format: [[x1, y1, z1], ...] or Nx3 array.
labels : List[str]
List of label strings to display at each point.
font_size : int, default: 12
Font size for the labels.
point_size : float, default: 5.0
Size of the point markers shown with labels.
**kwargs : dict
Additional keyword arguments passed to PyVista's add_point_labels method.

Returns
-------
pv.Actor
PyVista actor representing the added labels.
"""
import numpy as np

# Convert points to numpy array if needed
points_array = np.asarray(points)

# Ensure points are 2D with shape (N, 3)
if points_array.ndim == 1:
points_array = points_array.reshape(-1, 3)

# Create PyVista PolyData from points
point_cloud = pv.PolyData(points_array)

# Add point labels to the scene
actor = self._pl.scene.add_point_labels(
point_cloud,
labels,
font_size=font_size,
point_size=point_size,
**kwargs
)

return actor

def clear(self) -> None:
Comment thread
moe-ad marked this conversation as resolved.
"""Clear all actors from the scene and reset the plotter.

This method removes all previously added objects (meshes, points, lines,
text, etc.) from the visualization scene by fully reinitializing the
plotter.
"""
# Clean up existing resources
self._cleanup()
# Reinitialize with saved parameters
self.__init__(**self._init_params)
Comment thread
RobPasMue marked this conversation as resolved.
Loading
Loading