Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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/465.miscellaneous.md
Comment thread
moe-ad marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feat: Add customization APIs
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
29 changes: 26 additions & 3 deletions examples/00-basic-pyvista-examples/customization_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,34 @@


# 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='white')

# 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')
plotter.add_text("PyVista Backend", position="upper_left", font_size=12, color='lightblue')
plotter.add_text("3D Visualization", position="upper_right", font_size=12, color='lightgreen')
Comment thread
moe-ad marked this conversation as resolved.
Outdated


# 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_point_labels(label_points, labels, font_size=16, point_size=8.0)


# Note: In PyVista, clear() must be called BEFORE show(). Once show() is called,
# the plotter cannot be reused. Typical workflow: build scene -> clear -> rebuild -> show().
# Therefore, clear() method is mainly useful for resetting the scene during interactive work.

# Uncomment to clear everything added above and start fresh:
# 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
22 changes: 22 additions & 0 deletions examples/01-basic-plotly-examples/customization_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@
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_point_labels(label_points, labels, font_size=16, point_size=8.0)


# Note: Unlike PyVista, Plotly allows reuse after show(). Therefore, the
# clear method can be used for resetting the scene at any point.

# 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
52 changes: 52 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,55 @@ def add_text(
Backend-specific actor or object representing the added text.
"""
raise NotImplementedError("add_text method must be implemented")

@abstractmethod
def add_point_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_point_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.

Notes
-----
Backend-specific behavior:

- **PyVista backend**: This method must be called BEFORE ``show()``.
Once ``show()`` is called, the PyVista plotter becomes unusable and
cannot be reused. This is primarily useful in interactive sessions
where you build a scene, clear it, rebuild it differently, then show.
- **Plotly backend**: No such restriction exists. The plotter can be
cleared and reused even after calling ``show()``.
"""
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_point_labels(
Comment thread
moe-ad marked this conversation as resolved.
Outdated
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 = []
108 changes: 79 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 @@ -59,6 +58,7 @@
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.color import Color
from ansys.tools.visualization_interface.utils.helpers import extract_kwargs
from ansys.tools.visualization_interface.utils.logger import logger

_HAS_TRAME = importlib.util.find_spec("pyvista.trame") and importlib.util.find_spec("trame.app")
Expand Down Expand Up @@ -370,31 +370,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 +405,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 @@ -785,7 +760,6 @@ def add_points(
point_cloud,
color=color,
point_size=size,
render_points_as_spheres=True,
**kwargs
)

Expand Down Expand Up @@ -952,3 +926,79 @@ def add_text(
)

return actor

def add_point_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.

This method removes all previously added objects (meshes, points, lines,
text, etc.) from the visualization scene.

Notes
-----
This method must be called BEFORE ``show()``. PyVista plotters cannot
be reused after ``show()`` has been called. Calling this method after
``show()`` will have no effect as the plotter is no longer usable.
This method is primarily useful in interactive sessions where you want
to modify the scene before displaying it. Typical workflow:

1. Add objects to the scene
2. Optionally call ``clear()`` to reset
3. Add different objects
4. Call ``show()`` once to display

Do not use a pattern like: add objects -> show() -> clear() -> add objects.
This will not work with PyVista backend.
"""
self._pl.scene.clear()
Loading
Loading