diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 1f4009f8..67aee464 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -10,10 +10,11 @@ on: - main env: - MAIN_PYTHON_VERSION: '3.11' + MAIN_PYTHON_VERSION: '3.12' RESET_IMAGE_CACHE: 0 PACKAGE_NAME: ansys-tools-visualization-interface DOCUMENTATION_CNAME: visualization-interface.tools.docs.pyansys.com + IN_GITHUB_ACTIONS: true concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -104,6 +105,8 @@ jobs: needs: [ smoke-tests ] runs-on: ubuntu-latest steps: + - name: Install system dependencies + run: sudo apt install libegl1 libxcb-cursor0 libsm6 libxext6 libxcb-xinerama0 -y - name: Restore images cache uses: actions/cache@v4 with: diff --git a/doc/changelog.d/192.added.md b/doc/changelog.d/192.added.md new file mode 100644 index 00000000..d1c8d379 --- /dev/null +++ b/doc/changelog.d/192.added.md @@ -0,0 +1 @@ +feat: Add PyVista Qt support \ No newline at end of file diff --git a/examples/00-basic-pyvista-examples/qt_backend.py b/examples/00-basic-pyvista-examples/qt_backend.py new file mode 100644 index 00000000..baf8ff05 --- /dev/null +++ b/examples/00-basic-pyvista-examples/qt_backend.py @@ -0,0 +1,71 @@ +# Copyright (C) 2023 - 2024 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. + +""" +.. _ref_backgroundplotter: + +======================== +Use a PyVista Qt backend +======================== + +PyVista Qt is a package that extends the PyVista functionality through the +usage of Qt. Qt applications operate in a separate thread than VTK, you can +simultaneously have an active VTK plot and a non-blocking Python session. + +This example shows how to use the PyVista Qt backend to create a plotter +""" + +import pyvista as pv + +from ansys.tools.visualization_interface import Plotter +from ansys.tools.visualization_interface.backends.pyvista import PyVistaBackend + +######################### +# Open a pyvistaqt window +# ======================= +# .. code-block:: python +# +# cube = pv.Cube() +# pv_backend = PyVistaBackend(use_qt=True) +# pl = Plotter(backend=pv_backend) +# pl.plot(cube) +# pl.show() +# + + +##################### +# Parallel VTK window +# =================== + +sphere = pv.Sphere() + +pl_parallel = Plotter() +pl_parallel.plot(sphere) +pl_parallel.show() + +############################ +# Close the pyvistaqt window +# ========================== +# .. code-block:: python +# +# pv_backend.close() +# diff --git a/pyproject.toml b/pyproject.toml index 160ed22d..b3568b33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,19 @@ dependencies = [ ] [project.optional-dependencies] -tests = ["pytest==8.3.3", "pytest-pyvista==0.1.9", "pytest-cov==6.0.0"] +pyvistaqt = [ + "pyside6 >= 6.8.0,<7", + "pyvistaqt >= 0.11.1,<1", +] +tests = [ + "pytest==8.3.3", + "pytest-pyvista==0.1.9", + "pytest-cov==6.0.0", + "pyside6==6.7.3", + "pyvistaqt==0.11.1,<1", + "pytest-qt" +] + doc = [ "ansys-sphinx-theme==1.2.0", "jupyter_sphinx==0.5.3", diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index a7fc01fd..c24a8379 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -84,6 +84,12 @@ class PyVistaBackendInterface(BaseBackend): allow_hovering : Optional[bool], default: False Whether to allow hovering capabilities in the window. Incompatible with picking. Picking will take precedence over hovering. + plot_picked_names : Optional[bool], default: False + Whether to plot the names of the picked objects. + show_plane : Optional[bool], default: False + Whether to show the plane in the plotter. + use_qt : Optional[bool], default: False + Whether to use the Qt backend for the plotter. """ def __init__( @@ -93,13 +99,14 @@ def __init__( allow_hovering: Optional[bool] = False, plot_picked_names: Optional[bool] = False, show_plane: Optional[bool] = False, + use_qt: Optional[bool] = False, **plotter_kwargs, ) -> None: """Initialize the ``use_trame`` parameter and save the current ``pv.OFF_SCREEN`` value.""" # Check if the use of trame was requested if use_trame is None: use_trame = ansys.tools.visualization_interface.USE_TRAME - + self._use_qt = use_qt self._use_trame = use_trame self._allow_picking = allow_picking self._allow_hovering = allow_hovering @@ -146,7 +153,7 @@ def __init__( logger.warning(warn_msg) self._pl = PyVistaInterface(show_plane=show_plane) else: - self._pl = PyVistaInterface(show_plane=show_plane) + self._pl = PyVistaInterface(show_plane=show_plane, use_qt=use_qt, **plotter_kwargs) self._enable_widgets = self._pl._enable_widgets @@ -175,7 +182,8 @@ def enable_widgets(self): ] self._widgets.append(MeasureWidget(self)) self._widgets.append(ScreenshotButton(self)) - self._widgets.append(MeshSliderWidget(self)) + if not self._use_qt: + self._widgets.append(MeshSliderWidget(self)) self._widgets.append(HideButton(self)) def add_widget(self, widget: Union[PlotterWidget, List[PlotterWidget]]): @@ -541,10 +549,11 @@ def __init__( use_trame: Optional[bool] = None, allow_picking: Optional[bool] = False, allow_hovering: Optional[bool] = False, - plot_picked_names: Optional[bool] = True + plot_picked_names: Optional[bool] = True, + use_qt: Optional[bool] = False ) -> None: """Initialize the generic plotter.""" - super().__init__(use_trame, allow_picking, allow_hovering, plot_picked_names) + super().__init__(use_trame, allow_picking, allow_hovering, plot_picked_names, use_qt=use_qt) def plot_iter( self, @@ -591,3 +600,7 @@ def plot(self, plottable_object: Any, name_filter: str = None, **plotting_option else: self.pv_interface.plot(plottable_object, name_filter, **plotting_options) + def close(self): + """Close the plotter for PyVistaQT.""" + if self._use_qt: + self.pv_interface.scene.close() 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 c8ecfe1c..8d2110ee 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Provides plotting for various PyAnsys objects.""" +import importlib import re from typing import Any, Dict, List, Optional, Union @@ -33,6 +34,9 @@ from ansys.tools.visualization_interface.utils.color import Color from ansys.tools.visualization_interface.utils.logger import logger +_HAS_PYVISTAQT = importlib.util.find_spec("pyvistaqt") +if _HAS_PYVISTAQT: + import pyvistaqt class PyVistaInterface: """Provides the middle class between PyVista plotting operations and PyAnsys objects. @@ -58,6 +62,8 @@ class PyVistaInterface: for visualization. show_plane : bool, default: False Whether to show the XY plane in the plotter window. + use_qt : bool, default: False + Whether to use the Qt backend for the plotter window. """ @@ -68,15 +74,27 @@ def __init__( num_points: int = 100, enable_widgets: bool = True, show_plane: bool = False, + use_qt: bool = False, **plotter_kwargs, ) -> None: """Initialize the plotter.""" # Generate custom scene if ``None`` is provided if scene is None: if viz_interface.TESTING_MODE: - scene = pv.Plotter(off_screen=True, **plotter_kwargs) + if use_qt and _HAS_PYVISTAQT: + scene = pyvistaqt.BackgroundPlotter(off_screen=True) + else: + if use_qt and not _HAS_PYVISTAQT: + message = "PyVistaQt dependency is not installed. Install it with " + \ + "`pip install ansys-tools-visualization-interface[pyvistaqt]`." + logger.warning(message) + scene = pv.Plotter(off_screen=True, **plotter_kwargs) + elif use_qt: + scene = pyvistaqt.BackgroundPlotter() else: scene = pv.Plotter(**plotter_kwargs) + + self._use_qt = use_qt # If required, use a white background with no gradient if not color_opts: color_opts = dict(color="white") @@ -91,8 +109,9 @@ def __init__( # Show the XY plane self._show_plane = show_plane + if not use_qt: + self.scene.add_axes(interactive=False) - self.scene.add_axes(interactive=False) # objects to actors mapping self._object_to_actors_map = {} self._enable_widgets = enable_widgets @@ -335,7 +354,10 @@ def show( if jupyter_backend: self.scene.show(jupyter_backend=jupyter_backend, **kwargs) else: - self.scene.show(**kwargs) + if self._use_qt: + self.scene.show() + else: + self.scene.show(**kwargs) def set_add_mesh_defaults(self, plotting_options: Optional[Dict]) -> None: """Set the default values for the plotting options. diff --git a/src/ansys/tools/visualization_interface/plotter.py b/src/ansys/tools/visualization_interface/plotter.py index 64734a69..33bfbfd3 100644 --- a/src/ansys/tools/visualization_interface/plotter.py +++ b/src/ansys/tools/visualization_interface/plotter.py @@ -81,4 +81,4 @@ def show( screenshot=screenshot, name_filter=name_filter, **plotting_options - ) + ) diff --git a/tests/test_generic_plotter.py b/tests/test_generic_plotter.py index 77b6df0f..b931b77e 100644 --- a/tests/test_generic_plotter.py +++ b/tests/test_generic_plotter.py @@ -20,12 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Test module for the generic plotter.""" +import os from pathlib import Path import numpy as np +import pytest import pyvista as pv from ansys.tools.visualization_interface import ClipPlane, MeshObjectPlot, Plotter +from ansys.tools.visualization_interface.backends.pyvista import PyVistaBackend + +IN_GITHUB_ACTIONS = os.getenv("IN_GITHUB_ACTIONS") == "true" class CustomTestClass: @@ -43,6 +48,16 @@ def test_plotter_add_pd(): pl.plot(sphere) pl.show() +@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Qt breaks CICD.") +def test_plotter_pyvistaqt(): + """Adds polydata to the plotter.""" + qt_backend = PyVistaBackend(use_qt=True) + pl = Plotter(backend=qt_backend) + sphere = pv.Sphere() + pl.plot(sphere) + # PyVista QT show() breaks PyTest, so we avoid it. + qt_backend.close() + def test_plotter_add_mb(): """Adds multiblock to the plotter."""