Skip to content

Commit 7b1b62c

Browse files
feat: Add PyVista Qt support (#192)
Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent d20e133 commit 7b1b62c

File tree

8 files changed

+148
-11
lines changed

8 files changed

+148
-11
lines changed

.github/workflows/ci_cd.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ on:
1010
- main
1111

1212
env:
13-
MAIN_PYTHON_VERSION: '3.11'
13+
MAIN_PYTHON_VERSION: '3.12'
1414
RESET_IMAGE_CACHE: 0
1515
PACKAGE_NAME: ansys-tools-visualization-interface
1616
DOCUMENTATION_CNAME: visualization-interface.tools.docs.pyansys.com
17+
IN_GITHUB_ACTIONS: true
1718

1819
concurrency:
1920
group: ${{ github.workflow }}-${{ github.ref }}
@@ -104,6 +105,8 @@ jobs:
104105
needs: [ smoke-tests ]
105106
runs-on: ubuntu-latest
106107
steps:
108+
- name: Install system dependencies
109+
run: sudo apt install libegl1 libxcb-cursor0 libsm6 libxext6 libxcb-xinerama0 -y
107110
- name: Restore images cache
108111
uses: actions/cache@v4
109112
with:

doc/changelog.d/192.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
feat: Add PyVista Qt support
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""
24+
.. _ref_backgroundplotter:
25+
26+
========================
27+
Use a PyVista Qt backend
28+
========================
29+
30+
PyVista Qt is a package that extends the PyVista functionality through the
31+
usage of Qt. Qt applications operate in a separate thread than VTK, you can
32+
simultaneously have an active VTK plot and a non-blocking Python session.
33+
34+
This example shows how to use the PyVista Qt backend to create a plotter
35+
"""
36+
37+
import pyvista as pv
38+
39+
from ansys.tools.visualization_interface import Plotter
40+
from ansys.tools.visualization_interface.backends.pyvista import PyVistaBackend
41+
42+
#########################
43+
# Open a pyvistaqt window
44+
# =======================
45+
# .. code-block:: python
46+
#
47+
# cube = pv.Cube()
48+
# pv_backend = PyVistaBackend(use_qt=True)
49+
# pl = Plotter(backend=pv_backend)
50+
# pl.plot(cube)
51+
# pl.show()
52+
#
53+
54+
55+
#####################
56+
# Parallel VTK window
57+
# ===================
58+
59+
sphere = pv.Sphere()
60+
61+
pl_parallel = Plotter()
62+
pl_parallel.plot(sphere)
63+
pl_parallel.show()
64+
65+
############################
66+
# Close the pyvistaqt window
67+
# ==========================
68+
# .. code-block:: python
69+
#
70+
# pv_backend.close()
71+
#

pyproject.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,19 @@ dependencies = [
3030
]
3131

3232
[project.optional-dependencies]
33-
tests = ["pytest==8.3.3", "pytest-pyvista==0.1.9", "pytest-cov==6.0.0"]
33+
pyvistaqt = [
34+
"pyside6 >= 6.8.0,<7",
35+
"pyvistaqt >= 0.11.1,<1",
36+
]
37+
tests = [
38+
"pytest==8.3.3",
39+
"pytest-pyvista==0.1.9",
40+
"pytest-cov==6.0.0",
41+
"pyside6==6.7.3",
42+
"pyvistaqt==0.11.1,<1",
43+
"pytest-qt"
44+
]
45+
3446
doc = [
3547
"ansys-sphinx-theme==1.2.0",
3648
"jupyter_sphinx==0.5.3",

src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ class PyVistaBackendInterface(BaseBackend):
8484
allow_hovering : Optional[bool], default: False
8585
Whether to allow hovering capabilities in the window. Incompatible with picking.
8686
Picking will take precedence over hovering.
87+
plot_picked_names : Optional[bool], default: False
88+
Whether to plot the names of the picked objects.
89+
show_plane : Optional[bool], default: False
90+
Whether to show the plane in the plotter.
91+
use_qt : Optional[bool], default: False
92+
Whether to use the Qt backend for the plotter.
8793
"""
8894

8995
def __init__(
@@ -93,13 +99,14 @@ def __init__(
9399
allow_hovering: Optional[bool] = False,
94100
plot_picked_names: Optional[bool] = False,
95101
show_plane: Optional[bool] = False,
102+
use_qt: Optional[bool] = False,
96103
**plotter_kwargs,
97104
) -> None:
98105
"""Initialize the ``use_trame`` parameter and save the current ``pv.OFF_SCREEN`` value."""
99106
# Check if the use of trame was requested
100107
if use_trame is None:
101108
use_trame = ansys.tools.visualization_interface.USE_TRAME
102-
109+
self._use_qt = use_qt
103110
self._use_trame = use_trame
104111
self._allow_picking = allow_picking
105112
self._allow_hovering = allow_hovering
@@ -146,7 +153,7 @@ def __init__(
146153
logger.warning(warn_msg)
147154
self._pl = PyVistaInterface(show_plane=show_plane)
148155
else:
149-
self._pl = PyVistaInterface(show_plane=show_plane)
156+
self._pl = PyVistaInterface(show_plane=show_plane, use_qt=use_qt, **plotter_kwargs)
150157

151158
self._enable_widgets = self._pl._enable_widgets
152159

@@ -175,7 +182,8 @@ def enable_widgets(self):
175182
]
176183
self._widgets.append(MeasureWidget(self))
177184
self._widgets.append(ScreenshotButton(self))
178-
self._widgets.append(MeshSliderWidget(self))
185+
if not self._use_qt:
186+
self._widgets.append(MeshSliderWidget(self))
179187
self._widgets.append(HideButton(self))
180188

181189
def add_widget(self, widget: Union[PlotterWidget, List[PlotterWidget]]):
@@ -541,10 +549,11 @@ def __init__(
541549
use_trame: Optional[bool] = None,
542550
allow_picking: Optional[bool] = False,
543551
allow_hovering: Optional[bool] = False,
544-
plot_picked_names: Optional[bool] = True
552+
plot_picked_names: Optional[bool] = True,
553+
use_qt: Optional[bool] = False
545554
) -> None:
546555
"""Initialize the generic plotter."""
547-
super().__init__(use_trame, allow_picking, allow_hovering, plot_picked_names)
556+
super().__init__(use_trame, allow_picking, allow_hovering, plot_picked_names, use_qt=use_qt)
548557

549558
def plot_iter(
550559
self,
@@ -591,3 +600,7 @@ def plot(self, plottable_object: Any, name_filter: str = None, **plotting_option
591600
else:
592601
self.pv_interface.plot(plottable_object, name_filter, **plotting_options)
593602

603+
def close(self):
604+
"""Close the plotter for PyVistaQT."""
605+
if self._use_qt:
606+
self.pv_interface.scene.close()

src/ansys/tools/visualization_interface/backends/pyvista/pyvista_interface.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
# SOFTWARE.
2222
"""Provides plotting for various PyAnsys objects."""
23+
import importlib
2324
import re
2425
from typing import Any, Dict, List, Optional, Union
2526

@@ -33,6 +34,9 @@
3334
from ansys.tools.visualization_interface.utils.color import Color
3435
from ansys.tools.visualization_interface.utils.logger import logger
3536

37+
_HAS_PYVISTAQT = importlib.util.find_spec("pyvistaqt")
38+
if _HAS_PYVISTAQT:
39+
import pyvistaqt
3640

3741
class PyVistaInterface:
3842
"""Provides the middle class between PyVista plotting operations and PyAnsys objects.
@@ -58,6 +62,8 @@ class PyVistaInterface:
5862
for visualization.
5963
show_plane : bool, default: False
6064
Whether to show the XY plane in the plotter window.
65+
use_qt : bool, default: False
66+
Whether to use the Qt backend for the plotter window.
6167
6268
"""
6369

@@ -68,15 +74,27 @@ def __init__(
6874
num_points: int = 100,
6975
enable_widgets: bool = True,
7076
show_plane: bool = False,
77+
use_qt: bool = False,
7178
**plotter_kwargs,
7279
) -> None:
7380
"""Initialize the plotter."""
7481
# Generate custom scene if ``None`` is provided
7582
if scene is None:
7683
if viz_interface.TESTING_MODE:
77-
scene = pv.Plotter(off_screen=True, **plotter_kwargs)
84+
if use_qt and _HAS_PYVISTAQT:
85+
scene = pyvistaqt.BackgroundPlotter(off_screen=True)
86+
else:
87+
if use_qt and not _HAS_PYVISTAQT:
88+
message = "PyVistaQt dependency is not installed. Install it with " + \
89+
"`pip install ansys-tools-visualization-interface[pyvistaqt]`."
90+
logger.warning(message)
91+
scene = pv.Plotter(off_screen=True, **plotter_kwargs)
92+
elif use_qt:
93+
scene = pyvistaqt.BackgroundPlotter()
7894
else:
7995
scene = pv.Plotter(**plotter_kwargs)
96+
97+
self._use_qt = use_qt
8098
# If required, use a white background with no gradient
8199
if not color_opts:
82100
color_opts = dict(color="white")
@@ -91,8 +109,9 @@ def __init__(
91109

92110
# Show the XY plane
93111
self._show_plane = show_plane
112+
if not use_qt:
113+
self.scene.add_axes(interactive=False)
94114

95-
self.scene.add_axes(interactive=False)
96115
# objects to actors mapping
97116
self._object_to_actors_map = {}
98117
self._enable_widgets = enable_widgets
@@ -335,7 +354,10 @@ def show(
335354
if jupyter_backend:
336355
self.scene.show(jupyter_backend=jupyter_backend, **kwargs)
337356
else:
338-
self.scene.show(**kwargs)
357+
if self._use_qt:
358+
self.scene.show()
359+
else:
360+
self.scene.show(**kwargs)
339361

340362
def set_add_mesh_defaults(self, plotting_options: Optional[Dict]) -> None:
341363
"""Set the default values for the plotting options.

src/ansys/tools/visualization_interface/plotter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ def show(
8181
screenshot=screenshot,
8282
name_filter=name_filter,
8383
**plotting_options
84-
)
84+
)

tests/test_generic_plotter.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
# SOFTWARE.
2222
"""Test module for the generic plotter."""
23+
import os
2324
from pathlib import Path
2425

2526
import numpy as np
27+
import pytest
2628
import pyvista as pv
2729

2830
from ansys.tools.visualization_interface import ClipPlane, MeshObjectPlot, Plotter
31+
from ansys.tools.visualization_interface.backends.pyvista import PyVistaBackend
32+
33+
IN_GITHUB_ACTIONS = os.getenv("IN_GITHUB_ACTIONS") == "true"
2934

3035

3136
class CustomTestClass:
@@ -43,6 +48,16 @@ def test_plotter_add_pd():
4348
pl.plot(sphere)
4449
pl.show()
4550

51+
@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Qt breaks CICD.")
52+
def test_plotter_pyvistaqt():
53+
"""Adds polydata to the plotter."""
54+
qt_backend = PyVistaBackend(use_qt=True)
55+
pl = Plotter(backend=qt_backend)
56+
sphere = pv.Sphere()
57+
pl.plot(sphere)
58+
# PyVista QT show() breaks PyTest, so we avoid it.
59+
qt_backend.close()
60+
4661

4762
def test_plotter_add_mb():
4863
"""Adds multiblock to the plotter."""

0 commit comments

Comments
 (0)