From d0b2fd6848f02d17f0e78c9a77baa6a6385071a8 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:28:03 +0000 Subject: [PATCH 01/19] chore: adding changelog file 465.miscellaneous.md [dependabot-skip] --- doc/changelog.d/465.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/465.miscellaneous.md diff --git a/doc/changelog.d/465.miscellaneous.md b/doc/changelog.d/465.miscellaneous.md new file mode 100644 index 000000000..bb700c6e8 --- /dev/null +++ b/doc/changelog.d/465.miscellaneous.md @@ -0,0 +1 @@ +Feat: Add customization APIs From 37c8d433e26b0016f0093c0925ea5b5ee6182e86 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Sun, 8 Feb 2026 18:42:20 +0100 Subject: [PATCH 02/19] feat: add more customization apis --- .../visualization_interface/backends/_base.py | 79 ++++++++ .../backends/plotly/plotly_interface.py | 144 +++++++++++++-- .../backends/pyvista/pyvista.py | 170 +++++++++++++++++- .../tools/visualization_interface/plotter.py | 160 ++++++++++++++++- 4 files changed, 527 insertions(+), 26 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/_base.py b/src/ansys/tools/visualization_interface/backends/_base.py index 8698877e0..2cedd46fb 100644 --- a/src/ansys/tools/visualization_interface/backends/_base.py +++ b/src/ansys/tools/visualization_interface/backends/_base.py @@ -170,3 +170,82 @@ def add_text( Backend-specific actor or object representing the added text. """ raise NotImplementedError("add_text method must be implemented") + + @abstractmethod + def add_mesh( + self, + mesh: Any, + scalars: Optional[Union[str, Any]] = None, + scalar_bar_args: Optional[dict] = None, + show_edges: bool = False, + nan_color: str = "grey", + **kwargs + ) -> Any: + """Add a mesh to the scene. + + Parameters + ---------- + mesh : Any + Mesh object to add. Can be a PyVista mesh (UnstructuredGrid, PolyData, + MultiBlock) or other backend-specific mesh type. + scalars : Optional[Union[str, Any]], default: None + Scalars to use for coloring. Can be a string name of an array in + the mesh, or an array-like object with scalar values. + scalar_bar_args : Optional[dict], default: None + Arguments for the scalar bar (colorbar). Common keys include: + - 'title': Title for the scalar bar + - 'vertical': Whether to orient vertically (default False) + show_edges : bool, default: False + Whether to show mesh edges. + nan_color : str, default: "grey" + Color to use for NaN values in scalars. + **kwargs : dict + Additional backend-specific keyword arguments. + + Returns + ------- + Any + Backend-specific actor or object representing the added mesh. + """ + raise NotImplementedError("add_mesh 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. + """ + raise NotImplementedError("clear method must be implemented") diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index 6cd07478f..26eb5ee2c 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -478,19 +478,135 @@ def add_text( dict Plotly annotation representing the added text. """ - # 2D annotation with normalized coordinates - annotation = dict( - x=position[0] if len(position) > 0 else 0, - y=position[1] if len(position) > 1 else 0, - text=text, - font=dict( - size=font_size, - color=color, - ), - showarrow=False, - xref="paper", - yref="paper", + # For Plotly, we'll use Scatter3d with text mode for 3D text + if len(position) == 3: + # 3D text using scatter points with text + text_trace = go.Scatter3d( + x=[position[0]], + y=[position[1]], + z=[position[2]], + mode='text', + text=[text], + textfont=dict( + size=font_size, + color=color, + ), + **kwargs + ) + self._fig.add_trace(text_trace) + return text_trace + else: + # 2D annotation + annotation = dict( + x=position[0] if len(position) > 0 else 0, + y=position[1] if len(position) > 1 else 0, + text=text, + font=dict( + size=font_size, + color=color, + ), + showarrow=False, + xref="paper", + yref="paper", + **kwargs + ) + self._fig.add_annotation(annotation) + return annotation + + def add_mesh( + self, + mesh: Any, + scalars: Optional[Union[str, Any]] = None, + scalar_bar_args: Optional[dict] = None, + show_edges: bool = False, + nan_color: str = "grey", + **kwargs + ) -> Any: + """Add a mesh to the scene. + + Note: This is a simplified implementation. For full mesh rendering + in Plotly, see the plot() method which converts meshes to Mesh3d. + + Parameters + ---------- + mesh : Any + Mesh object to add. + scalars : Optional[Union[str, Any]], default: None + Scalars to use for coloring. + scalar_bar_args : Optional[dict], default: None + Arguments for the scalar bar. + show_edges : bool, default: False + Whether to show mesh edges. + nan_color : str, default: "grey" + Color to use for NaN values. + **kwargs : dict + Additional keyword arguments. + + Returns + ------- + Any + Plotly trace representing the mesh. + """ + # Use the existing conversion method to convert the mesh + mesh3d = self._pv_to_mesh3d(mesh) + + if isinstance(mesh3d, list): + # MultiBlock case - add all traces + for trace in mesh3d: + self._fig.add_trace(trace) + return mesh3d + else: + self._fig.add_trace(mesh3d) + return mesh3d + + 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. + 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_annotation(annotation) - return annotation + self._fig.add_trace(trace) + return trace + + def clear(self) -> None: + """Clear all traces from the figure.""" + self._fig.data = [] diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index a36ca7405..560acd392 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -923,13 +923,12 @@ def add_text( ---------- text : str Text string to display. - position : Union[Tuple[float, float], str] + position : Union[Tuple[float, float], Tuple[float, float, float], str] Position for the text. Can be: - - - 2D tuple (x, y) for screen coordinates (pixels from bottom-left) + - 2D tuple (x, y) for screen coordinates + - 3D tuple (x, y, z) for world coordinates - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (PyVista-specific) - font_size : int, default: 12 Font size for the text. color : str, default: "white" @@ -942,13 +941,166 @@ def add_text( pv.Actor PyVista actor representing the added text. """ - # Handle string positions or 2D coordinates - actor = self._pl.scene.add_text( - text, - position=position, + # Handle string positions (PyVista-specific) + if isinstance(position, str): + actor = self._pl.scene.add_text( + text, + position=position, + font_size=font_size, + color=color, + **kwargs + ) + return actor + + # Determine if position is 2D or 3D + if len(position) == 2: + # 2D screen coordinates - use add_text + actor = self._pl.scene.add_text( + text, + position=position, + font_size=font_size, + color=color, + **kwargs + ) + elif len(position) == 3: + # 3D world coordinates - create 3D text mesh + # We use Text3D instead of add_point_labels to avoid a bug in PyVista + # where it tries to access a non-existent _actors attribute + + # Create 3D text object + text_mesh = pv.Text3D(text, depth=0.0) + + # Scale text based on font_size (approximate scaling factor) + scale_factor = font_size / 100.0 + text_mesh.points *= scale_factor + + # Translate text to desired position + text_mesh.translate(position, inplace=True) + + # Add text mesh to scene with specified color + actor = self._pl.scene.add_mesh( + text_mesh, + color=color, + **kwargs + ) + else: + raise ValueError( + f"Position must be 2D (x, y) or 3D (x, y, z), got {len(position)} dimensions" + ) + + return actor + + def add_mesh( + self, + mesh: Any, + scalars: Optional[Union[str, Any]] = None, + scalar_bar_args: Optional[dict] = None, + show_edges: bool = False, + nan_color: str = "grey", + **kwargs + ) -> "pv.Actor": + """Add a mesh to the scene. + + Parameters + ---------- + mesh : Any + Mesh object to add. Accepts PyVista mesh types including + UnstructuredGrid, PolyData, and MultiBlock. + scalars : Optional[Union[str, Any]], default: None + Scalars to use for coloring. Can be a string name of an array in + the mesh, or an array-like object with scalar values. + scalar_bar_args : Optional[dict], default: None + Arguments for the scalar bar (colorbar). Common keys include: + - 'title': Title for the scalar bar + - 'vertical': Whether to orient vertically + show_edges : bool, default: False + Whether to show mesh edges. + nan_color : str, default: "grey" + Color to use for NaN values in scalars. + **kwargs : dict + Additional keyword arguments passed to PyVista's add_mesh method. + + Returns + ------- + pv.Actor + PyVista actor representing the added mesh. + """ + # Build kwargs for PyVista add_mesh + add_mesh_kwargs = { + "show_edges": show_edges, + "nan_color": nan_color, + **kwargs + } + + # Add scalars if provided + if scalars is not None: + add_mesh_kwargs["scalars"] = scalars + + # Add scalar bar args if provided + if scalar_bar_args is not None: + add_mesh_kwargs["scalar_bar_args"] = scalar_bar_args + + # Add mesh to the scene + actor = self._pl.scene.add_mesh(mesh, **add_mesh_kwargs) + + 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, - color=color, + point_size=point_size, **kwargs ) return actor + + 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. + """ + self._pl.scene.clear() diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 14dac59c1..086cddf5d 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -401,7 +401,7 @@ def add_planes( def add_text( self, text: str, - position: Union[Tuple[float, float], str], + position: Union[Tuple[float, float], Tuple[float, float, float], str], font_size: int = 12, color: str = "white", **kwargs @@ -415,13 +415,13 @@ def add_text( ---------- text : str Text string to display. - position : Union[Tuple[float, float], str] + position : Union[Tuple[float, float], Tuple[float, float, float], str] Position for the text. Can be: - 2D tuple (x, y) for screen/viewport coordinates (pixels from bottom-left) + - 3D tuple (x, y, z) for world coordinates (backend-dependent support) - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (backend-dependent support) - font_size : int, default: 12 Font size for the text in points. color : str, default: "white" @@ -458,3 +458,157 @@ def add_text( return self._backend.add_text( text=text, position=position, font_size=font_size, color=color, **kwargs ) + + def add_mesh( + self, + mesh: Any, + scalars: Optional[Union[str, Any]] = None, + scalar_bar_args: Optional[dict] = None, + show_edges: bool = False, + nan_color: str = "grey", + **kwargs + ) -> Any: + """Add a mesh to the scene. + + This method provides a backend-agnostic way to add mesh objects to the + visualization scene. Meshes can be colored by scalar values with + customizable color bars. + + Parameters + ---------- + mesh : Any + Mesh object to add. Can be a PyVista mesh (UnstructuredGrid, PolyData, + MultiBlock) or other backend-specific mesh type. + scalars : Optional[Union[str, Any]], default: None + Scalars to use for coloring. Can be a string name of an array in + the mesh, or an array-like object with scalar values. + scalar_bar_args : Optional[dict], default: None + Arguments for the scalar bar (colorbar). Common keys include: + - 'title': Title for the scalar bar + - 'vertical': Whether to orient vertically (default False) + show_edges : bool, default: False + Whether to show mesh edges. + nan_color : str, default: "grey" + Color to use for NaN values in scalars. + **kwargs : dict + Additional backend-specific keyword arguments for advanced customization + (e.g., cmap, opacity, lighting). + + Returns + ------- + Any + Backend-specific actor or object representing the added mesh. + Can be used for further manipulation or removal. + + Examples + -------- + Add a simple mesh: + + >>> from ansys.tools.visualization_interface import Plotter + >>> import pyvista as pv + >>> plotter = Plotter() + >>> mesh = pv.Sphere() + >>> plotter.add_mesh(mesh, show_edges=True) + >>> plotter.show() + + Add a mesh with scalar coloring: + + >>> mesh = pv.Sphere() + >>> mesh['elevation'] = mesh.points[:, 2] + >>> plotter.add_mesh( + ... mesh, + ... scalars='elevation', + ... scalar_bar_args={'title': 'Elevation (m)'}, + ... cmap='viridis' + ... ) + >>> plotter.show() + """ + return self._backend.add_mesh( + mesh=mesh, + scalars=scalars, + scalar_bar_args=scalar_bar_args, + show_edges=show_edges, + nan_color=nan_color, + **kwargs + ) + + 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. + + This method provides a backend-agnostic way to add text labels at + specific 3D coordinates in the visualization scene. Labels are + displayed next to marker points. + + 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. Must have the same + length as points. + 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 for advanced customization + (e.g., text_color, shape, fill_shape). + + Returns + ------- + Any + Backend-specific actor or object representing the added labels. + Can be used for further manipulation or removal. + + Examples + -------- + Add labels at specific locations: + + >>> from ansys.tools.visualization_interface import Plotter + >>> plotter = Plotter() + >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] + >>> labels = ['Origin', 'X-axis', 'Y-axis'] + >>> plotter.add_point_labels(points, labels, font_size=14) + >>> plotter.show() + + Add labels with custom styling: + + >>> plotter.add_point_labels( + ... points, + ... labels, + ... font_size=16, + ... point_size=10, + ... text_color='yellow', + ... shape='rounded_rect' + ... ) + >>> plotter.show() + """ + return self._backend.add_point_labels( + points=points, labels=labels, font_size=font_size, point_size=point_size, **kwargs + ) + + 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. + + Examples + -------- + >>> from ansys.tools.visualization_interface import Plotter + >>> import pyvista as pv + >>> plotter = Plotter() + >>> plotter.add_mesh(pv.Sphere()) + >>> plotter.clear() # Remove all objects + >>> plotter.add_mesh(pv.Cube()) # Add different object + >>> plotter.show() + """ + self._backend.clear() From 897b435c8eeefa2bf7ac9c149a95f792fecbf09c Mon Sep 17 00:00:00 2001 From: moe-ad Date: Mon, 9 Feb 2026 15:59:07 +0100 Subject: [PATCH 03/19] wip --- .../visualization_interface/backends/plotly/plotly_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index 26eb5ee2c..c4dcb42ab 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -21,7 +21,7 @@ # SOFTWARE. """Plotly backend interface for visualization.""" -from typing import Any, Iterable, List, Optional, Tuple, Union +from typing import Any, Iterable, List, Optional, Union import plotly.graph_objects as go import pyvista as pv From 518d4b0d42a743760430bd9ea34d6667d477ddac Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:11:28 +0000 Subject: [PATCH 04/19] chore: adding changelog file 466.miscellaneous.md [dependabot-skip] --- doc/changelog.d/466.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/466.miscellaneous.md diff --git a/doc/changelog.d/466.miscellaneous.md b/doc/changelog.d/466.miscellaneous.md new file mode 100644 index 000000000..664bf6f63 --- /dev/null +++ b/doc/changelog.d/466.miscellaneous.md @@ -0,0 +1 @@ +Feat: more customization APIs From 5cd3d33b55acca1ba7307ceb7c0b5428ab4d2f2b Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 10 Feb 2026 07:32:31 +0100 Subject: [PATCH 05/19] checkpoint --- .../visualization_interface/backends/_base.py | 38 -------- .../backends/plotly/plotly_interface.py | 46 ---------- .../backends/pyvista/pyvista.py | 86 +------------------ .../backends/pyvista/pyvista_interface.py | 7 +- .../tools/visualization_interface/plotter.py | 73 ---------------- .../visualization_interface/utils/helpers.py | 67 +++++++++++++++ 6 files changed, 75 insertions(+), 242 deletions(-) create mode 100644 src/ansys/tools/visualization_interface/utils/helpers.py diff --git a/src/ansys/tools/visualization_interface/backends/_base.py b/src/ansys/tools/visualization_interface/backends/_base.py index 2cedd46fb..b2384b33c 100644 --- a/src/ansys/tools/visualization_interface/backends/_base.py +++ b/src/ansys/tools/visualization_interface/backends/_base.py @@ -171,44 +171,6 @@ def add_text( """ raise NotImplementedError("add_text method must be implemented") - @abstractmethod - def add_mesh( - self, - mesh: Any, - scalars: Optional[Union[str, Any]] = None, - scalar_bar_args: Optional[dict] = None, - show_edges: bool = False, - nan_color: str = "grey", - **kwargs - ) -> Any: - """Add a mesh to the scene. - - Parameters - ---------- - mesh : Any - Mesh object to add. Can be a PyVista mesh (UnstructuredGrid, PolyData, - MultiBlock) or other backend-specific mesh type. - scalars : Optional[Union[str, Any]], default: None - Scalars to use for coloring. Can be a string name of an array in - the mesh, or an array-like object with scalar values. - scalar_bar_args : Optional[dict], default: None - Arguments for the scalar bar (colorbar). Common keys include: - - 'title': Title for the scalar bar - - 'vertical': Whether to orient vertically (default False) - show_edges : bool, default: False - Whether to show mesh edges. - nan_color : str, default: "grey" - Color to use for NaN values in scalars. - **kwargs : dict - Additional backend-specific keyword arguments. - - Returns - ------- - Any - Backend-specific actor or object representing the added mesh. - """ - raise NotImplementedError("add_mesh method must be implemented") - @abstractmethod def add_point_labels( self, diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index c4dcb42ab..65288a272 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -513,52 +513,6 @@ def add_text( self._fig.add_annotation(annotation) return annotation - def add_mesh( - self, - mesh: Any, - scalars: Optional[Union[str, Any]] = None, - scalar_bar_args: Optional[dict] = None, - show_edges: bool = False, - nan_color: str = "grey", - **kwargs - ) -> Any: - """Add a mesh to the scene. - - Note: This is a simplified implementation. For full mesh rendering - in Plotly, see the plot() method which converts meshes to Mesh3d. - - Parameters - ---------- - mesh : Any - Mesh object to add. - scalars : Optional[Union[str, Any]], default: None - Scalars to use for coloring. - scalar_bar_args : Optional[dict], default: None - Arguments for the scalar bar. - show_edges : bool, default: False - Whether to show mesh edges. - nan_color : str, default: "grey" - Color to use for NaN values. - **kwargs : dict - Additional keyword arguments. - - Returns - ------- - Any - Plotly trace representing the mesh. - """ - # Use the existing conversion method to convert the mesh - mesh3d = self._pv_to_mesh3d(mesh) - - if isinstance(mesh3d, list): - # MultiBlock case - add all traces - for trace in mesh3d: - self._fig.add_trace(trace) - return mesh3d - else: - self._fig.add_trace(mesh3d) - return mesh3d - def add_point_labels( self, points: Union[List, Any], diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index 560acd392..a9d0aa44f 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -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 @@ -35,6 +34,7 @@ InMemoryFrameSequence, ) from ansys.tools.visualization_interface.backends.pyvista.picker import AbstractPicker, Picker +from ansys.tools.visualization_interface.utils.helpers import extract_kwargs from ansys.tools.visualization_interface.backends.pyvista.pyvista_interface import PyVistaInterface from ansys.tools.visualization_interface.backends.pyvista.widgets.dark_mode import DarkModeButton from ansys.tools.visualization_interface.backends.pyvista.widgets.displace_arrows import ( @@ -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, @@ -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, ) @@ -990,61 +965,6 @@ def add_text( return actor - def add_mesh( - self, - mesh: Any, - scalars: Optional[Union[str, Any]] = None, - scalar_bar_args: Optional[dict] = None, - show_edges: bool = False, - nan_color: str = "grey", - **kwargs - ) -> "pv.Actor": - """Add a mesh to the scene. - - Parameters - ---------- - mesh : Any - Mesh object to add. Accepts PyVista mesh types including - UnstructuredGrid, PolyData, and MultiBlock. - scalars : Optional[Union[str, Any]], default: None - Scalars to use for coloring. Can be a string name of an array in - the mesh, or an array-like object with scalar values. - scalar_bar_args : Optional[dict], default: None - Arguments for the scalar bar (colorbar). Common keys include: - - 'title': Title for the scalar bar - - 'vertical': Whether to orient vertically - show_edges : bool, default: False - Whether to show mesh edges. - nan_color : str, default: "grey" - Color to use for NaN values in scalars. - **kwargs : dict - Additional keyword arguments passed to PyVista's add_mesh method. - - Returns - ------- - pv.Actor - PyVista actor representing the added mesh. - """ - # Build kwargs for PyVista add_mesh - add_mesh_kwargs = { - "show_edges": show_edges, - "nan_color": nan_color, - **kwargs - } - - # Add scalars if provided - if scalars is not None: - add_mesh_kwargs["scalars"] = scalars - - # Add scalar bar args if provided - if scalar_bar_args is not None: - add_mesh_kwargs["scalar_bar_args"] = scalar_bar_args - - # Add mesh to the scene - actor = self._pl.scene.add_mesh(mesh, **add_mesh_kwargs) - - return actor - def add_point_labels( self, points: Union[List, Any], diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py index e83d80aa3..dbc2d37dc 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -30,6 +30,7 @@ import ansys.tools.visualization_interface as viz_interface from ansys.tools.visualization_interface.types.edge_plot import EdgePlot from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot +from ansys.tools.visualization_interface.utils.helpers import extract_kwargs from ansys.tools.visualization_interface.utils.clip_plane import ClipPlane from ansys.tools.visualization_interface.utils.color import Color from ansys.tools.visualization_interface.utils.logger import logger @@ -92,11 +93,13 @@ def __init__( logger.warning(message) # Avoiding having duplicated argument plotter_kwargs.pop("off_screen", None) - scene = pv.Plotter(off_screen=True, **plotter_kwargs) + filtered_kwargs = extract_kwargs(pv.Plotter, plotter_kwargs) + scene = pv.Plotter(off_screen=True, **filtered_kwargs) elif use_qt: scene = pyvistaqt.BackgroundPlotter(show=show_qt) else: - scene = pv.Plotter(**plotter_kwargs) + filtered_kwargs = extract_kwargs(pv.Plotter, plotter_kwargs) + scene = pv.Plotter(**filtered_kwargs) self._use_qt = use_qt # If required, use a white background with no gradient diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 086cddf5d..00c0c68d2 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -459,79 +459,6 @@ def add_text( text=text, position=position, font_size=font_size, color=color, **kwargs ) - def add_mesh( - self, - mesh: Any, - scalars: Optional[Union[str, Any]] = None, - scalar_bar_args: Optional[dict] = None, - show_edges: bool = False, - nan_color: str = "grey", - **kwargs - ) -> Any: - """Add a mesh to the scene. - - This method provides a backend-agnostic way to add mesh objects to the - visualization scene. Meshes can be colored by scalar values with - customizable color bars. - - Parameters - ---------- - mesh : Any - Mesh object to add. Can be a PyVista mesh (UnstructuredGrid, PolyData, - MultiBlock) or other backend-specific mesh type. - scalars : Optional[Union[str, Any]], default: None - Scalars to use for coloring. Can be a string name of an array in - the mesh, or an array-like object with scalar values. - scalar_bar_args : Optional[dict], default: None - Arguments for the scalar bar (colorbar). Common keys include: - - 'title': Title for the scalar bar - - 'vertical': Whether to orient vertically (default False) - show_edges : bool, default: False - Whether to show mesh edges. - nan_color : str, default: "grey" - Color to use for NaN values in scalars. - **kwargs : dict - Additional backend-specific keyword arguments for advanced customization - (e.g., cmap, opacity, lighting). - - Returns - ------- - Any - Backend-specific actor or object representing the added mesh. - Can be used for further manipulation or removal. - - Examples - -------- - Add a simple mesh: - - >>> from ansys.tools.visualization_interface import Plotter - >>> import pyvista as pv - >>> plotter = Plotter() - >>> mesh = pv.Sphere() - >>> plotter.add_mesh(mesh, show_edges=True) - >>> plotter.show() - - Add a mesh with scalar coloring: - - >>> mesh = pv.Sphere() - >>> mesh['elevation'] = mesh.points[:, 2] - >>> plotter.add_mesh( - ... mesh, - ... scalars='elevation', - ... scalar_bar_args={'title': 'Elevation (m)'}, - ... cmap='viridis' - ... ) - >>> plotter.show() - """ - return self._backend.add_mesh( - mesh=mesh, - scalars=scalars, - scalar_bar_args=scalar_bar_args, - show_edges=show_edges, - nan_color=nan_color, - **kwargs - ) - def add_point_labels( self, points: Union[List, Any], diff --git a/src/ansys/tools/visualization_interface/utils/helpers.py b/src/ansys/tools/visualization_interface/utils/helpers.py new file mode 100644 index 000000000..fac184cee --- /dev/null +++ b/src/ansys/tools/visualization_interface/utils/helpers.py @@ -0,0 +1,67 @@ +# Copyright (C) 2024 - 2026 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Utilities for filtering and extracting keyword arguments.""" +import inspect +from typing import Any, Callable, Dict + + +def extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]: + """Extract keyword arguments that match a function's signature. + + This function inspects the signature of a callable and returns a dictionary + containing only the keyword arguments that the function accepts. For each + parameter with a default value, it uses the value from ``input_kwargs`` if + present, otherwise it uses the parameter's default value. + + Parameters + ---------- + func : Callable + Function to extract the keyword arguments from. + input_kwargs : Dict[str, Any] + Dictionary with keyword arguments to filter. + + Returns + ------- + Dict[str, Any] + Dictionary containing only the keyword arguments that match the + function's signature, with values from ``input_kwargs`` or defaults. + + Examples + -------- + >>> def my_func(a, b=1, c=2): + ... pass + >>> extract_kwargs(my_func, {"b": 10, "d": 20}) + {"b": 10, "c": 2} + + Notes + ----- + - Only parameters with default values are included in the output. + - Positional-only parameters without defaults are ignored. + - This is useful for filtering kwargs before passing to PyVista functions. + """ + signature = inspect.signature(func) + 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 From b363b17b376f046e7a7ee643a6dc3ffd16f2e4e4 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 10 Feb 2026 07:50:03 +0100 Subject: [PATCH 06/19] checkpoint --- .../backends/pyvista/pyvista_interface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py index dbc2d37dc..7518ce78e 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -30,7 +30,6 @@ import ansys.tools.visualization_interface as viz_interface from ansys.tools.visualization_interface.types.edge_plot import EdgePlot from ansys.tools.visualization_interface.types.mesh_object_plot import MeshObjectPlot -from ansys.tools.visualization_interface.utils.helpers import extract_kwargs from ansys.tools.visualization_interface.utils.clip_plane import ClipPlane from ansys.tools.visualization_interface.utils.color import Color from ansys.tools.visualization_interface.utils.logger import logger @@ -93,12 +92,10 @@ def __init__( logger.warning(message) # Avoiding having duplicated argument plotter_kwargs.pop("off_screen", None) - filtered_kwargs = extract_kwargs(pv.Plotter, plotter_kwargs) scene = pv.Plotter(off_screen=True, **filtered_kwargs) elif use_qt: scene = pyvistaqt.BackgroundPlotter(show=show_qt) else: - filtered_kwargs = extract_kwargs(pv.Plotter, plotter_kwargs) scene = pv.Plotter(**filtered_kwargs) self._use_qt = use_qt From 91aeb9bb90b4ea187e90f3744da5e37bf63f3339 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:50:52 +0000 Subject: [PATCH 07/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../visualization_interface/backends/plotly/plotly_interface.py | 2 +- .../tools/visualization_interface/backends/pyvista/pyvista.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index 65288a272..c7d7e494f 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -21,7 +21,7 @@ # SOFTWARE. """Plotly backend interface for visualization.""" -from typing import Any, Iterable, List, Optional, Union +from typing import Any, Iterable, List, Union import plotly.graph_objects as go import pyvista as pv diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index a9d0aa44f..ef97f5d50 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -34,7 +34,6 @@ InMemoryFrameSequence, ) from ansys.tools.visualization_interface.backends.pyvista.picker import AbstractPicker, Picker -from ansys.tools.visualization_interface.utils.helpers import extract_kwargs from ansys.tools.visualization_interface.backends.pyvista.pyvista_interface import PyVistaInterface from ansys.tools.visualization_interface.backends.pyvista.widgets.dark_mode import DarkModeButton from ansys.tools.visualization_interface.backends.pyvista.widgets.displace_arrows import ( @@ -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") From 2bd21222da2d082d762a54239ab0792bbec79598 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 10 Feb 2026 07:53:38 +0100 Subject: [PATCH 08/19] checkpoint --- .../backends/pyvista/pyvista_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py index 7518ce78e..08e211e69 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -25,6 +25,7 @@ from typing import Any, Dict, List, Optional, Union import pyvista as pv +from pyvista.plotting import plotter from pyvista.plotting.plotter import Plotter as PyVistaPlotter import ansys.tools.visualization_interface as viz_interface @@ -92,11 +93,11 @@ def __init__( logger.warning(message) # Avoiding having duplicated argument plotter_kwargs.pop("off_screen", None) - scene = pv.Plotter(off_screen=True, **filtered_kwargs) + scene = pv.Plotter(off_screen=True, **plotter_kwargs) elif use_qt: scene = pyvistaqt.BackgroundPlotter(show=show_qt) else: - scene = pv.Plotter(**filtered_kwargs) + scene = pv.Plotter(**plotter_kwargs) self._use_qt = use_qt # If required, use a white background with no gradient From d12600dae793e0663f002c115926dd120ea12e45 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 06:53:50 +0000 Subject: [PATCH 09/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../backends/pyvista/pyvista_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py index 08e211e69..e83d80aa3 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -25,7 +25,6 @@ from typing import Any, Dict, List, Optional, Union import pyvista as pv -from pyvista.plotting import plotter from pyvista.plotting.plotter import Plotter as PyVistaPlotter import ansys.tools.visualization_interface as viz_interface From 82279ed5c8d329735d054fa8440cc2681db620dd Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 10 Feb 2026 11:38:22 +0100 Subject: [PATCH 10/19] fix: remove render points as spheres --- .../tools/visualization_interface/backends/pyvista/pyvista.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index ef97f5d50..fd971d0cb 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -760,7 +760,6 @@ def add_points( point_cloud, color=color, point_size=size, - render_points_as_spheres=True, **kwargs ) From 614572f245c077605a4b5e406740d5f7b8a6489a Mon Sep 17 00:00:00 2001 From: moe-ad Date: Mon, 16 Feb 2026 08:34:31 +0100 Subject: [PATCH 11/19] chore: sync with main --- .../backends/plotly/plotly_interface.py | 52 ++++++----------- .../backends/pyvista/pyvista.py | 56 +++---------------- .../tools/visualization_interface/plotter.py | 5 +- 3 files changed, 28 insertions(+), 85 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index c7d7e494f..0b6b296c3 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -21,7 +21,7 @@ # SOFTWARE. """Plotly backend interface for visualization.""" -from typing import Any, Iterable, List, Union +from typing import Any, Iterable, List, Optional, Tuple, Union import plotly.graph_objects as go import pyvista as pv @@ -478,40 +478,22 @@ def add_text( dict Plotly annotation representing the added text. """ - # For Plotly, we'll use Scatter3d with text mode for 3D text - if len(position) == 3: - # 3D text using scatter points with text - text_trace = go.Scatter3d( - x=[position[0]], - y=[position[1]], - z=[position[2]], - mode='text', - text=[text], - textfont=dict( - size=font_size, - color=color, - ), - **kwargs - ) - self._fig.add_trace(text_trace) - return text_trace - else: - # 2D annotation - annotation = dict( - x=position[0] if len(position) > 0 else 0, - y=position[1] if len(position) > 1 else 0, - text=text, - font=dict( - size=font_size, - color=color, - ), - showarrow=False, - xref="paper", - yref="paper", - **kwargs - ) - self._fig.add_annotation(annotation) - return annotation + # 2D annotation with normalized coordinates + annotation = dict( + x=position[0] if len(position) > 0 else 0, + y=position[1] if len(position) > 1 else 0, + text=text, + font=dict( + size=font_size, + color=color, + ), + showarrow=False, + xref="paper", + yref="paper", + **kwargs + ) + self._fig.add_annotation(annotation) + return annotation def add_point_labels( self, diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index fd971d0cb..00d1cdb11 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -899,7 +899,7 @@ def add_text( Text string to display. position : Union[Tuple[float, float], Tuple[float, float, float], str] Position for the text. Can be: - - 2D tuple (x, y) for screen coordinates + - 2D tuple (x, y) for screen coordinates (pixels from bottom-left) - 3D tuple (x, y, z) for world coordinates - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (PyVista-specific) @@ -915,52 +915,14 @@ def add_text( pv.Actor PyVista actor representing the added text. """ - # Handle string positions (PyVista-specific) - if isinstance(position, str): - actor = self._pl.scene.add_text( - text, - position=position, - font_size=font_size, - color=color, - **kwargs - ) - return actor - - # Determine if position is 2D or 3D - if len(position) == 2: - # 2D screen coordinates - use add_text - actor = self._pl.scene.add_text( - text, - position=position, - font_size=font_size, - color=color, - **kwargs - ) - elif len(position) == 3: - # 3D world coordinates - create 3D text mesh - # We use Text3D instead of add_point_labels to avoid a bug in PyVista - # where it tries to access a non-existent _actors attribute - - # Create 3D text object - text_mesh = pv.Text3D(text, depth=0.0) - - # Scale text based on font_size (approximate scaling factor) - scale_factor = font_size / 100.0 - text_mesh.points *= scale_factor - - # Translate text to desired position - text_mesh.translate(position, inplace=True) - - # Add text mesh to scene with specified color - actor = self._pl.scene.add_mesh( - text_mesh, - color=color, - **kwargs - ) - else: - raise ValueError( - f"Position must be 2D (x, y) or 3D (x, y, z), got {len(position)} dimensions" - ) + # Handle string positions or 2D coordinates + actor = self._pl.scene.add_text( + text, + position=position, + font_size=font_size, + color=color, + **kwargs + ) return actor diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 00c0c68d2..60c56dd7b 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -401,7 +401,7 @@ def add_planes( def add_text( self, text: str, - position: Union[Tuple[float, float], Tuple[float, float, float], str], + position: Union[Tuple[float, float], str], font_size: int = 12, color: str = "white", **kwargs @@ -415,11 +415,10 @@ def add_text( ---------- text : str Text string to display. - position : Union[Tuple[float, float], Tuple[float, float, float], str] + position : Union[Tuple[float, float], str] Position for the text. Can be: - 2D tuple (x, y) for screen/viewport coordinates (pixels from bottom-left) - - 3D tuple (x, y, z) for world coordinates (backend-dependent support) - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (backend-dependent support) font_size : int, default: 12 From af00dcd064f15af60d0c723614f741a71e8c28fe Mon Sep 17 00:00:00 2001 From: moe-ad Date: Mon, 16 Feb 2026 08:37:30 +0100 Subject: [PATCH 12/19] chore: sync with main --- .../tools/visualization_interface/backends/pyvista/pyvista.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index 00d1cdb11..c67e637c7 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -897,10 +897,9 @@ def add_text( ---------- text : str Text string to display. - position : Union[Tuple[float, float], Tuple[float, float, float], str] + position : Union[Tuple[float, float], str] Position for the text. Can be: - 2D tuple (x, y) for screen coordinates (pixels from bottom-left) - - 3D tuple (x, y, z) for world coordinates - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (PyVista-specific) font_size : int, default: 12 From b9a7e011eab0f7afcabb93c77e19d55a17d46f25 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Mon, 16 Feb 2026 12:54:09 +0100 Subject: [PATCH 13/19] fix: doc-build --- .../backends/pyvista/pyvista.py | 2 ++ .../tools/visualization_interface/utils/helpers.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index c67e637c7..137d23a19 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -899,9 +899,11 @@ def add_text( Text string to display. position : Union[Tuple[float, float], str] Position for the text. Can be: + - 2D tuple (x, y) for screen coordinates (pixels from bottom-left) - String position like 'upper_left', 'upper_right', 'lower_left', 'lower_right', 'upper_edge', 'lower_edge' (PyVista-specific) + font_size : int, default: 12 Font size for the text. color : str, default: "white" diff --git a/src/ansys/tools/visualization_interface/utils/helpers.py b/src/ansys/tools/visualization_interface/utils/helpers.py index fac184cee..6fbbdb16e 100644 --- a/src/ansys/tools/visualization_interface/utils/helpers.py +++ b/src/ansys/tools/visualization_interface/utils/helpers.py @@ -45,18 +45,18 @@ def extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, An Dictionary containing only the keyword arguments that match the function's signature, with values from ``input_kwargs`` or defaults. + Notes + ----- + - Only parameters with default values are included in the output. + - Positional-only parameters without defaults are ignored. + - This is useful for filtering kwargs before passing to PyVista functions. + Examples -------- >>> def my_func(a, b=1, c=2): ... pass >>> extract_kwargs(my_func, {"b": 10, "d": 20}) {"b": 10, "c": 2} - - Notes - ----- - - Only parameters with default values are included in the output. - - Positional-only parameters without defaults are ignored. - - This is useful for filtering kwargs before passing to PyVista functions. """ signature = inspect.signature(func) kwargs = {} From 1e42bfca268f8038f9a6a4ab723bfffe78ce6f04 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Mon, 16 Feb 2026 15:45:03 +0100 Subject: [PATCH 14/19] feat: add tests, examples, and clear() docstrings --- .../customization_api.py | 29 ++++++++-- .../customization_api.py | 22 ++++++++ .../visualization_interface/backends/_base.py | 11 ++++ .../backends/pyvista/pyvista.py | 16 ++++++ .../tools/visualization_interface/plotter.py | 26 +++++++-- tests/test_customization_api.py | 54 +++++++++++++++++++ 6 files changed, 152 insertions(+), 6 deletions(-) diff --git a/examples/00-basic-pyvista-examples/customization_api.py b/examples/00-basic-pyvista-examples/customization_api.py index b550370e5..56fd594eb 100644 --- a/examples/00-basic-pyvista-examples/customization_api.py +++ b/examples/00-basic-pyvista-examples/customization_api.py @@ -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') + + +# 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 + # Display the visualization with all customizations. diff --git a/examples/01-basic-plotly-examples/customization_api.py b/examples/01-basic-plotly-examples/customization_api.py index e369e2151..a1af8b63c 100644 --- a/examples/01-basic-plotly-examples/customization_api.py +++ b/examples/01-basic-plotly-examples/customization_api.py @@ -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 + + # Display the visualization with all customizations. diff --git a/src/ansys/tools/visualization_interface/backends/_base.py b/src/ansys/tools/visualization_interface/backends/_base.py index b2384b33c..0449b71c4 100644 --- a/src/ansys/tools/visualization_interface/backends/_base.py +++ b/src/ansys/tools/visualization_interface/backends/_base.py @@ -209,5 +209,16 @@ def clear(self) -> None: 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") diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index 137d23a19..2e164eb61 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -984,5 +984,21 @@ def clear(self) -> None: 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() diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 60c56dd7b..c76ee732b 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -527,14 +527,34 @@ def clear(self) -> None: This method removes all previously added objects (meshes, points, lines, text, etc.) from the visualization scene. + Notes + ----- + Backend-specific behavior: + + - **PyVista**: Plotter becomes unusable after ``show()`` is called. + Use ``clear()`` only before showing to modify the scene. + - **Plotly**: No restrictions. Plotter can be cleared and reused after + ``show()``. + Examples -------- + Correct usage - clear before show: + >>> from ansys.tools.visualization_interface import Plotter >>> import pyvista as pv >>> plotter = Plotter() >>> plotter.add_mesh(pv.Sphere()) - >>> plotter.clear() # Remove all objects - >>> plotter.add_mesh(pv.Cube()) # Add different object - >>> plotter.show() + >>> plotter.clear() # Changed mind before showing, remove sphere + >>> plotter.add_mesh(pv.Cube()) # Add cube instead + >>> plotter.show() # Display cube only + + Interactive session workflow: + + >>> plotter = Plotter() + >>> plotter.plot(pv.Sphere()) # Try sphere + >>> plotter.show() # Show it + >>> plotter.clear() # Can be cleared after showing + >>> plotter.plot(pv.Cube()) # Use cube instead + >>> plotter.show() # Display cube """ self._backend.clear() diff --git a/tests/test_customization_api.py b/tests/test_customization_api.py index 697356fcb..9f30327af 100644 --- a/tests/test_customization_api.py +++ b/tests/test_customization_api.py @@ -21,6 +21,8 @@ # SOFTWARE. """Test module for the customization API methods.""" +import pyvista as pv + from ansys.tools.visualization_interface import Plotter from ansys.tools.visualization_interface.backends.plotly.plotly_interface import PlotlyBackend @@ -128,3 +130,55 @@ def test_add_text_plotly(): pl = Plotter(backend=PlotlyBackend()) annotation = pl.add_text("Test Label", position=(0.5, 0.9), font_size=14, color="yellow") assert annotation is not None + + +def test_add_point_labels_pyvista(): + """Test add_point_labels API with PyVista backend.""" + pl = Plotter() + points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] + labels = ['Point A', 'Point B', 'Point C'] + actor = pl.add_point_labels(points, labels, font_size=14, point_size=8.0) + assert actor is not None + pl.show() + + +def test_add_point_labels_plotly(): + """Test add_point_labels API with Plotly backend.""" + pl = Plotter(backend=PlotlyBackend()) + points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] + labels = ['Point A', 'Point B', 'Point C'] + trace = pl.add_point_labels(points, labels, font_size=14, point_size=8.0) + assert trace is not None + + +def test_clear_pyvista(): + """Test clear API with PyVista backend.""" + pl = Plotter() + # Add multiple objects + sphere = pv.Sphere() + pl.plot(sphere) + pl.add_points([[0, 0, 0], [1, 0, 0]], color='red', size=10) + pl.add_lines([[0, 0, 0], [1, 1, 1]], color='blue', width=2.0) + # Clear before show (required for PyVista) + pl.clear() + # Add new object to verify plotter still works + cube = pv.Cube() + pl.plot(cube) + pl.show() + + +def test_clear_plotly(): + """Test clear API with Plotly backend.""" + pl = Plotter(backend=PlotlyBackend()) + # Add multiple objects + sphere = pv.Sphere() + pl.plot(sphere) + pl.add_points([[0, 0, 0], [1, 0, 0]], color='red', size=10) + pl.add_lines([[0, 0, 0], [1, 1, 1]], color='blue', width=2.0) + # Clear the scene + pl.clear() + # Verify all traces removed + assert len(pl._backend._fig.data) == 0 + # Add new object to verify plotter still works + cube = pv.Cube() + pl.plot(cube) From 97a2bd899363eab65dc26e7d477ae7593e8c93c2 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 17 Feb 2026 10:25:36 +0100 Subject: [PATCH 15/19] fix: review suggestions --- doc/changelog.d/465.miscellaneous.md | 1 - .../custom_picker.py | 4 +- .../customization_api.py | 8 +- .../customization_api.py | 6 +- .../visualization_interface/backends/_base.py | 21 +++-- .../backends/plotly/plotly_interface.py | 2 +- .../backends/pyvista/pyvista.py | 88 ++++++++++++++----- .../tools/visualization_interface/plotter.py | 42 ++++----- tests/test_customization_api.py | 33 +++++-- 9 files changed, 129 insertions(+), 76 deletions(-) delete mode 100644 doc/changelog.d/465.miscellaneous.md diff --git a/doc/changelog.d/465.miscellaneous.md b/doc/changelog.d/465.miscellaneous.md deleted file mode 100644 index bb700c6e8..000000000 --- a/doc/changelog.d/465.miscellaneous.md +++ /dev/null @@ -1 +0,0 @@ -Feat: Add customization APIs diff --git a/examples/00-basic-pyvista-examples/custom_picker.py b/examples/00-basic-pyvista-examples/custom_picker.py index c5027e026..3f8afe641 100644 --- a/examples/00-basic-pyvista-examples/custom_picker.py +++ b/examples/00-basic-pyvista-examples/custom_picker.py @@ -105,7 +105,7 @@ def pick_select_object(self, custom_object: MeshObjectPlot, pt: "np.ndarray") -> # If picking names is enabled, add a label to the picked object if self._plot_picked_names: - label_actor = self._plotter_backend.pv_interface.scene.add_point_labels( + label_actor = self._plotter_backend.pv_interface.scene.add_labels( [pt], [self._label + text], always_visible=True, @@ -155,7 +155,7 @@ def hover_select_object(self, custom_object: MeshObjectPlot, actor: "Actor") -> """ for label in self._added_hover_labels: self._plotter_backend._pl.scene.remove_actor(label) - label_actor = self._plotter_backend._pl.scene.add_point_labels( + label_actor = self._plotter_backend._pl.scene.add_labels( [actor.GetCenter()], [custom_object.name], always_visible=True, diff --git a/examples/00-basic-pyvista-examples/customization_api.py b/examples/00-basic-pyvista-examples/customization_api.py index 56fd594eb..d64176bcc 100644 --- a/examples/00-basic-pyvista-examples/customization_api.py +++ b/examples/00-basic-pyvista-examples/customization_api.py @@ -100,14 +100,14 @@ labels = ['X-axis', 'Y-axis', 'Z-axis'] -plotter.add_point_labels(label_points, labels, font_size=16, point_size=8.0) +plotter.add_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. +# 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 diff --git a/examples/01-basic-plotly-examples/customization_api.py b/examples/01-basic-plotly-examples/customization_api.py index a1af8b63c..ac7d12408 100644 --- a/examples/01-basic-plotly-examples/customization_api.py +++ b/examples/01-basic-plotly-examples/customization_api.py @@ -103,11 +103,11 @@ labels = ['X-axis', 'Y-axis', 'Z-axis'] -plotter.add_point_labels(label_points, labels, font_size=16, point_size=8.0) +plotter.add_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. +# 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() diff --git a/src/ansys/tools/visualization_interface/backends/_base.py b/src/ansys/tools/visualization_interface/backends/_base.py index 0449b71c4..de920fe5b 100644 --- a/src/ansys/tools/visualization_interface/backends/_base.py +++ b/src/ansys/tools/visualization_interface/backends/_base.py @@ -172,7 +172,7 @@ def add_text( raise NotImplementedError("add_text method must be implemented") @abstractmethod - def add_point_labels( + def add_labels( self, points: Union[List, Any], labels: List[str], @@ -201,24 +201,23 @@ def add_point_labels( Any Backend-specific actor or object representing the added labels. """ - raise NotImplementedError("add_point_labels method must be implemented") + 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. + text, etc.) from the visualization scene, therefore allowing + the plotter to be reused after ``show()`` has been called. 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()``. + 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") diff --git a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py index 0b6b296c3..f4c134fa7 100644 --- a/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py +++ b/src/ansys/tools/visualization_interface/backends/plotly/plotly_interface.py @@ -495,7 +495,7 @@ def add_text( self._fig.add_annotation(annotation) return annotation - def add_point_labels( + def add_labels( self, points: Union[List, Any], labels: List[str], diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index 2e164eb61..a49968719 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -122,6 +122,20 @@ def __init__( from vtkmodules.vtkInteractionWidgets import vtkHoverWidget from vtkmodules.vtkRenderingCore import vtkPointPicker + # Save initialization parameters for potential reinitialization via clear() + self._init_params = { + 'use_trame': use_trame, + 'allow_picking': allow_picking, + 'allow_hovering': allow_hovering, + 'plot_picked_names': plot_picked_names, + 'show_plane': show_plane, + 'use_qt': use_qt, + 'show_qt': show_qt, + 'custom_picker': custom_picker, + 'custom_picker_kwargs': custom_picker_kwargs, + **plotter_kwargs, + } + # Check if the use of trame was requested if use_trame is None: use_trame = ansys.tools.visualization_interface.USE_TRAME @@ -184,6 +198,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: + pass # Widget may already be disabled + + # Disable picking if it was enabled + if self._allow_picking and self._pl is not None: + try: + self._pl.scene.disable_picking() + except Exception: + pass # Picking 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: + pass # Plotter may already be closed + @property def pv_interface(self) -> PyVistaInterface: """PyVista interface.""" @@ -563,6 +611,18 @@ def __init__( **plotter_kwargs, ) -> None: """Initialize the generic plotter.""" + # Save initialization parameters for reinitialization via clear() + self._init_params = { + 'use_trame': use_trame, + 'allow_picking': allow_picking, + 'allow_hovering': allow_hovering, + 'plot_picked_names': plot_picked_names, + 'use_qt': use_qt, + 'show_qt': show_qt, + 'custom_picker': custom_picker, + **plotter_kwargs, + } + super().__init__( use_trame, allow_picking, @@ -927,7 +987,7 @@ def add_text( return actor - def add_point_labels( + def add_labels( self, points: Union[List, Any], labels: List[str], @@ -980,25 +1040,13 @@ def add_point_labels( return actor def clear(self) -> None: - """Clear all actors from the scene. + """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. - - 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. + text, etc.) from the visualization scene by fully reinitializing the + plotter. """ - self._pl.scene.clear() + # Clean up existing resources + self._cleanup() + # Reinitialize with saved parameters + self.__init__(**self._init_params) diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index c76ee732b..81c05af82 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -458,7 +458,7 @@ def add_text( text=text, position=position, font_size=font_size, color=color, **kwargs ) - def add_point_labels( + def add_labels( self, points: Union[List, Any], labels: List[str], @@ -502,12 +502,12 @@ def add_point_labels( >>> plotter = Plotter() >>> points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] >>> labels = ['Origin', 'X-axis', 'Y-axis'] - >>> plotter.add_point_labels(points, labels, font_size=14) + >>> plotter.add_labels(points, labels, font_size=14) >>> plotter.show() Add labels with custom styling: - >>> plotter.add_point_labels( + >>> plotter.add_labels( ... points, ... labels, ... font_size=16, @@ -517,7 +517,7 @@ def add_point_labels( ... ) >>> plotter.show() """ - return self._backend.add_point_labels( + return self._backend.add_labels( points=points, labels=labels, font_size=font_size, point_size=point_size, **kwargs ) @@ -525,36 +525,28 @@ 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**: Plotter becomes unusable after ``show()`` is called. - Use ``clear()`` only before showing to modify the scene. - - **Plotly**: No restrictions. Plotter can be cleared and reused after - ``show()``. + text, etc.) from the visualization scene, therefore allowing + the plotter to be reused after ``show()`` has been called. Examples -------- - Correct usage - clear before show: + Clear before showing: >>> from ansys.tools.visualization_interface import Plotter >>> import pyvista as pv >>> plotter = Plotter() - >>> plotter.add_mesh(pv.Sphere()) - >>> plotter.clear() # Changed mind before showing, remove sphere - >>> plotter.add_mesh(pv.Cube()) # Add cube instead - >>> plotter.show() # Display cube only + >>> plotter.plot(pv.Sphere()) + >>> plotter.clear() # Changed mind, remove sphere + >>> plotter.plot(pv.Cube()) + >>> plotter.show() - Interactive session workflow: + Clear after showing >>> plotter = Plotter() - >>> plotter.plot(pv.Sphere()) # Try sphere - >>> plotter.show() # Show it - >>> plotter.clear() # Can be cleared after showing - >>> plotter.plot(pv.Cube()) # Use cube instead - >>> plotter.show() # Display cube + >>> plotter.plot(pv.Sphere()) + >>> plotter.show() + >>> plotter.clear() # Reset the plotter to reuse it + >>> plotter.plot(pv.Cube()) + >>> plotter.show() """ self._backend.clear() diff --git a/tests/test_customization_api.py b/tests/test_customization_api.py index 9f30327af..879d34ebf 100644 --- a/tests/test_customization_api.py +++ b/tests/test_customization_api.py @@ -132,34 +132,34 @@ def test_add_text_plotly(): assert annotation is not None -def test_add_point_labels_pyvista(): - """Test add_point_labels API with PyVista backend.""" +def test_add_labels_pyvista(): + """Test add_labels API with PyVista backend.""" pl = Plotter() points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] labels = ['Point A', 'Point B', 'Point C'] - actor = pl.add_point_labels(points, labels, font_size=14, point_size=8.0) + actor = pl.add_labels(points, labels, font_size=14, point_size=8.0) assert actor is not None pl.show() -def test_add_point_labels_plotly(): - """Test add_point_labels API with Plotly backend.""" +def test_add_labels_plotly(): + """Test add_labels API with Plotly backend.""" pl = Plotter(backend=PlotlyBackend()) points = [[0, 0, 0], [1, 0, 0], [0, 1, 0]] labels = ['Point A', 'Point B', 'Point C'] - trace = pl.add_point_labels(points, labels, font_size=14, point_size=8.0) + trace = pl.add_labels(points, labels, font_size=14, point_size=8.0) assert trace is not None -def test_clear_pyvista(): - """Test clear API with PyVista backend.""" +def test_clear_pyvista_before_show(): + """Test clear API with PyVista backend - clear before show.""" pl = Plotter() # Add multiple objects sphere = pv.Sphere() pl.plot(sphere) pl.add_points([[0, 0, 0], [1, 0, 0]], color='red', size=10) pl.add_lines([[0, 0, 0], [1, 1, 1]], color='blue', width=2.0) - # Clear before show (required for PyVista) + # Clear before show pl.clear() # Add new object to verify plotter still works cube = pv.Cube() @@ -167,6 +167,21 @@ def test_clear_pyvista(): pl.show() +def test_clear_pyvista_after_show(): + """Test clear API with PyVista backend - clear after show (reinitialization).""" + pl = Plotter() + # Add and show first object + sphere = pv.Sphere() + pl.plot(sphere) + pl.show() + # Clear after show - this should reinitialize the plotter + pl.clear() + # Add new object to verify plotter can be reused + cube = pv.Cube() + pl.plot(cube) + pl.show() + + def test_clear_plotly(): """Test clear API with Plotly backend.""" pl = Plotter(backend=PlotlyBackend()) From 6bab9bbdd42612f4f98014658df62c1f2b7df39c Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 17 Feb 2026 10:31:31 +0100 Subject: [PATCH 16/19] fix: revert unintended edit --- examples/00-basic-pyvista-examples/custom_picker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/00-basic-pyvista-examples/custom_picker.py b/examples/00-basic-pyvista-examples/custom_picker.py index 3f8afe641..c5027e026 100644 --- a/examples/00-basic-pyvista-examples/custom_picker.py +++ b/examples/00-basic-pyvista-examples/custom_picker.py @@ -105,7 +105,7 @@ def pick_select_object(self, custom_object: MeshObjectPlot, pt: "np.ndarray") -> # If picking names is enabled, add a label to the picked object if self._plot_picked_names: - label_actor = self._plotter_backend.pv_interface.scene.add_labels( + label_actor = self._plotter_backend.pv_interface.scene.add_point_labels( [pt], [self._label + text], always_visible=True, @@ -155,7 +155,7 @@ def hover_select_object(self, custom_object: MeshObjectPlot, actor: "Actor") -> """ for label in self._added_hover_labels: self._plotter_backend._pl.scene.remove_actor(label) - label_actor = self._plotter_backend._pl.scene.add_labels( + label_actor = self._plotter_backend._pl.scene.add_point_labels( [actor.GetCenter()], [custom_object.name], always_visible=True, From e27ef099382d2418426c5b8696f1a3c67afc09ef Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 17 Feb 2026 12:07:01 +0100 Subject: [PATCH 17/19] fix: review suggestions --- .../00-basic-pyvista-examples/customization_api.py | 7 ++++--- .../01-basic-plotly-examples/customization_api.py | 2 +- .../backends/pyvista/pyvista.py | 12 ++++++------ .../utils/{helpers.py => _helpers.py} | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) rename src/ansys/tools/visualization_interface/utils/{helpers.py => _helpers.py} (95%) diff --git a/examples/00-basic-pyvista-examples/customization_api.py b/examples/00-basic-pyvista-examples/customization_api.py index d64176bcc..62a7c2e16 100644 --- a/examples/00-basic-pyvista-examples/customization_api.py +++ b/examples/00-basic-pyvista-examples/customization_api.py @@ -83,11 +83,12 @@ # Scene title at the top center -plotter.add_text("Customization API Example", position="upper_edge", 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 +# 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') -plotter.add_text("3D Visualization", position="upper_right", font_size=12, color='lightgreen') +# 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. diff --git a/examples/01-basic-plotly-examples/customization_api.py b/examples/01-basic-plotly-examples/customization_api.py index ac7d12408..ea2a059f0 100644 --- a/examples/01-basic-plotly-examples/customization_api.py +++ b/examples/01-basic-plotly-examples/customization_api.py @@ -86,7 +86,7 @@ # 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') diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index a49968719..f1add7444 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -57,8 +57,8 @@ ) 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._helpers import _extract_kwargs 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") @@ -209,14 +209,14 @@ def _cleanup(self) -> None: try: self._hover_widget.EnabledOff() except Exception: - pass # Widget may already be disabled + 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: - pass # Picking may not be active + logger.warning("Failed to disable picking. It may not be active.") # Clear widgets list self._widgets.clear() @@ -230,7 +230,7 @@ def _cleanup(self) -> None: try: self._pl.scene.close() except Exception: - pass # Plotter may already be closed + logger.warning("Failed to close the plotter. It may already be closed.") @property def pv_interface(self) -> PyVistaInterface: @@ -453,11 +453,11 @@ def show( List with the picked bodies in the picked order. """ - plotting_options = extract_kwargs( + plotting_options = _extract_kwargs( self._pl._scene.add_mesh, kwargs, ) - show_options = extract_kwargs( + show_options = _extract_kwargs( self._pl.scene.show, kwargs, ) diff --git a/src/ansys/tools/visualization_interface/utils/helpers.py b/src/ansys/tools/visualization_interface/utils/_helpers.py similarity index 95% rename from src/ansys/tools/visualization_interface/utils/helpers.py rename to src/ansys/tools/visualization_interface/utils/_helpers.py index 6fbbdb16e..8e2c69921 100644 --- a/src/ansys/tools/visualization_interface/utils/helpers.py +++ b/src/ansys/tools/visualization_interface/utils/_helpers.py @@ -24,7 +24,7 @@ from typing import Any, Callable, Dict -def extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]: +def _extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]: """Extract keyword arguments that match a function's signature. This function inspects the signature of a callable and returns a dictionary @@ -55,7 +55,7 @@ def extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, An -------- >>> def my_func(a, b=1, c=2): ... pass - >>> extract_kwargs(my_func, {"b": 10, "d": 20}) + >>> _extract_kwargs(my_func, {"b": 10, "d": 20}) {"b": 10, "c": 2} """ signature = inspect.signature(func) From abfa1de5521db1b9141997ee7bfa15fe4aab22e8 Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 17 Feb 2026 12:28:29 +0100 Subject: [PATCH 18/19] fix: review suggestion --- .../tools/visualization_interface/backends/pyvista/pyvista.py | 2 +- .../utils/{_helpers.py => _kwargs_manager.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/ansys/tools/visualization_interface/utils/{_helpers.py => _kwargs_manager.py} (100%) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index f1add7444..9136319db 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -57,7 +57,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._helpers import _extract_kwargs +from ansys.tools.visualization_interface.utils._kwargs_manager import _extract_kwargs from ansys.tools.visualization_interface.utils.color import Color from ansys.tools.visualization_interface.utils.logger import logger diff --git a/src/ansys/tools/visualization_interface/utils/_helpers.py b/src/ansys/tools/visualization_interface/utils/_kwargs_manager.py similarity index 100% rename from src/ansys/tools/visualization_interface/utils/_helpers.py rename to src/ansys/tools/visualization_interface/utils/_kwargs_manager.py From 67e6a1c0a8a8f397396092c9dfda40ef96402a6d Mon Sep 17 00:00:00 2001 From: moe-ad Date: Tue, 17 Feb 2026 14:44:16 +0100 Subject: [PATCH 19/19] fix: review suggestions --- .../backends/pyvista/pyvista.py | 33 ++------ .../utils/_kwargs_manager.py | 81 ++++++++++++++++++- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index 9136319db..83ce09cef 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -57,7 +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 _extract_kwargs +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 @@ -123,18 +126,7 @@ def __init__( from vtkmodules.vtkRenderingCore import vtkPointPicker # Save initialization parameters for potential reinitialization via clear() - self._init_params = { - 'use_trame': use_trame, - 'allow_picking': allow_picking, - 'allow_hovering': allow_hovering, - 'plot_picked_names': plot_picked_names, - 'show_plane': show_plane, - 'use_qt': use_qt, - 'show_qt': show_qt, - 'custom_picker': custom_picker, - 'custom_picker_kwargs': custom_picker_kwargs, - **plotter_kwargs, - } + self._init_params = _capture_init_params(self.__init__, locals()) # Check if the use of trame was requested if use_trame is None: @@ -611,18 +603,6 @@ def __init__( **plotter_kwargs, ) -> None: """Initialize the generic plotter.""" - # Save initialization parameters for reinitialization via clear() - self._init_params = { - 'use_trame': use_trame, - 'allow_picking': allow_picking, - 'allow_hovering': allow_hovering, - 'plot_picked_names': plot_picked_names, - 'use_qt': use_qt, - 'show_qt': show_qt, - 'custom_picker': custom_picker, - **plotter_kwargs, - } - super().__init__( use_trame, allow_picking, @@ -634,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.""" diff --git a/src/ansys/tools/visualization_interface/utils/_kwargs_manager.py b/src/ansys/tools/visualization_interface/utils/_kwargs_manager.py index 8e2c69921..48e94fd2d 100644 --- a/src/ansys/tools/visualization_interface/utils/_kwargs_manager.py +++ b/src/ansys/tools/visualization_interface/utils/_kwargs_manager.py @@ -21,7 +21,7 @@ # SOFTWARE. """Utilities for filtering and extracting keyword arguments.""" import inspect -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Optional def _extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]: @@ -65,3 +65,82 @@ def _extract_kwargs(func: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, A if v.default is not inspect.Parameter.empty: kwargs[k] = input_kwargs[k] if k in input_kwargs else v.default return kwargs + + +def _capture_init_params( + func_or_method: Callable, + locals_dict: Dict[str, Any], + exclude: Optional[List[str]] = None, +) -> Dict[str, Any]: + """Capture initialization parameters from a function's locals for reinitialization. + + This function dynamically extracts all parameters passed to an ``__init__`` method + by inspecting its signature and reading values from the ``locals()`` dictionary. + This is particularly useful for implementing reinitialization patterns where the + exact initialization parameters need to be saved and replayed later. + + Parameters + ---------- + func_or_method : Callable + The function or method whose parameters should be captured. Typically + ``self.__init__`` or the class's ``__init__`` method. + locals_dict : Dict[str, Any] + The ``locals()`` dictionary from inside the ``__init__`` method. This + contains the actual values of all parameters at the point of capture. + exclude : Optional[List[str]], default: None + List of parameter names to exclude from the result. Common exclusions + are ``'self'`` and ``'cls'``, though ``'self'`` is automatically excluded. + + Returns + ------- + Dict[str, Any] + Dictionary mapping parameter names to their values, suitable for unpacking + with ``**`` when calling the function. If the signature includes ``**kwargs``, + those are flattened into the result dictionary. + + Examples + -------- + Using in a child class after super(): + + >>> class Parent: + ... def __init__(self, x, y): + ... self.x = x + ... self.y = y + ... + >>> class Child(Parent): + ... def __init__(self, x, y, z): + ... super().__init__(x, y) + ... # Capture after super() to avoid interference + ... self._init_params = _capture_init_params( + ... self.__init__, + ... locals() + ... ) + ... + >>> obj = Child(1, 2, 3) + >>> obj._init_params + {'x': 1, 'y': 2, 'z': 3} + """ + if exclude is None: + exclude = [] + + exclude_set = set(exclude) | {'self', 'cls'} + sig = inspect.signature(func_or_method) + result = {} + + for param_name, param in sig.parameters.items(): + if param_name in exclude_set: + continue + if param_name not in locals_dict: + continue + + # Handle VAR_KEYWORD (**kwargs) parameters + if param.kind == inspect.Parameter.VAR_KEYWORD: + # Flatten the kwargs into the result + kwargs_value = locals_dict[param_name] + if isinstance(kwargs_value, dict): + result.update(kwargs_value) + else: + # Regular parameter - add directly + result[param_name] = locals_dict[param_name] + + return result