From bd481f03943b4062b01b85c8a0388583b1e2307d Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Fri, 26 Sep 2025 15:39:54 +0200 Subject: [PATCH 1/3] feat: Add night mode and reorganize buttons --- .github/workflows/ci_cd.yml | 2 +- .../backends/pyvista/pyvista.py | 2 + .../pyvista/widgets/_images/dark_mode.png | Bin 0 -> 595 bytes .../pyvista/widgets/_images/dark_mode_inv.png | Bin 0 -> 495 bytes .../backends/pyvista/widgets/dark_mode.py | 111 ++++++++++++++++++ .../pyvista/widgets/displace_arrows.py | 12 +- .../backends/pyvista/widgets/measure.py | 2 +- .../backends/pyvista/widgets/mesh_slider.py | 2 +- .../pyvista/widgets/pick_rotation_center.py | 2 +- .../backends/pyvista/widgets/ruler.py | 2 +- .../backends/pyvista/widgets/screenshot.py | 2 +- .../backends/pyvista/widgets/view_button.py | 14 +-- tests/test_interactables.py | 63 ++++++---- 13 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode.png create mode 100644 src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode_inv.png create mode 100644 src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index e080555d..499115b3 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -11,7 +11,7 @@ on: env: MAIN_PYTHON_VERSION: '3.13' - RESET_IMAGE_CACHE: 0 + RESET_IMAGE_CACHE: 1 PACKAGE_NAME: ansys-tools-visualization-interface DOCUMENTATION_CNAME: visualization-interface.tools.docs.pyansys.com IN_GITHUB_ACTIONS: true diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py index d99ddedd..1c0cbe5d 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/pyvista.py @@ -33,6 +33,7 @@ import ansys.tools.visualization_interface from ansys.tools.visualization_interface.backends._base import BaseBackend 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 ( CameraPanDirection, DisplacementArrow, @@ -209,6 +210,7 @@ def enable_widgets(self, dark_mode: bool = False) -> None: self._widgets.append(MeshSliderWidget(self, dark_mode)) self._widgets.append(HideButton(self, dark_mode)) self._widgets.append(PickRotCenterButton(self, dark_mode)) + self._widgets.append(DarkModeButton(self, dark_mode)) def add_widget(self, widget: Union[PlotterWidget, List[PlotterWidget]]): """Add one or more custom widgets to the plotter. diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode.png b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..64626f758bf227f9466241f1e0adb18dff6ee71e GIT binary patch literal 595 zcmV-Z0<8UsP)9FJ7r6pXftSy{I&*RordN=NJ{j9uR;$(DVi#Qf zj(m*impG210!-%AOKt%-x1QB)wRd59Jf*J`!T(Rvu?3h-oX+QJRX3F7L_0|1XE0@lbB zp!o>%{jXf!+DUO7e1|l1eU}2pvmcK|>r4TfU)aPY%L8IG9gOcFK-`FE?IKlx2KOYF z5xJs)`Pfw|m3LhA8t5~UD)G$bihBYSn;#AhN=ks=<1>FYUbJg`Q$Xh)!l_rT1i=%a z$oz0}Y|U5RAF8VYVj_C2`YcX`)66@NqLgpC~VnQw8!0i^d-T00960cdlzi h00006NklPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TK9cIA|05J=W!~%)A!bqH_j|H=52LrVHf5wT@)~N=hB0eciVmo&1 zB#r(RbzOICGTn$f_@?59>D+vnQYzn?$m#a27h)a)d|PL!&Wn3T{Y*nL^5^ImTP4C ldqM%GnEj#69$;po=o{|D^XqYBy0HKN002ovPDHLkV1n8T))@c* literal 0 HcmV?d00001 diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py new file mode 100644 index 00000000..486b0407 --- /dev/null +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py @@ -0,0 +1,111 @@ +# Copyright (C) 2024 - 2025 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. +"""Provides the hide buttons widget for the PyAnsys plotter.""" +from pathlib import Path +from typing import TYPE_CHECKING + +from vtk import vtkActor, vtkButtonWidget, vtkPNGReader + +from ansys.tools.visualization_interface.backends.pyvista.widgets.widget import PlotterWidget + +if TYPE_CHECKING: + from ansys.tools.visualization_interface.backends.pyvista.pyvista import Plotter + + +class DarkModeButton(PlotterWidget): + """Provides the dark mode widget for the Visualization Interface Tool ``Plotter`` class. + + Parameters + ---------- + plotter_helper : PlotterHelper + Plotter to add the dark mode widget to. + dark_mode : bool, optional + Whether to activate the dark mode or not. + + """ + + def __init__(self, plotter: "Plotter", dark_mode: bool = False) -> None: + """Initialize the ``DarkModeButton`` class.""" + # Call PlotterWidget ctor + super().__init__(plotter._pl.scene) + self._dark_mode = dark_mode + # Initialize variables + self._actor: vtkActor = None + self._plotter = plotter + self._button: vtkButtonWidget = self._plotter._pl.scene.add_checkbox_button_widget( + self.callback, position=(43, 10), size=30, border_size=3 + ) + self.update() + + def callback(self, state: bool) -> None: + """Remove or add the dark mode widget actor upon click. + + Parameters + ---------- + state : bool + Whether the state of the button, which is inherited from PyVista, is active. + + """ + if not self._dark_mode: + self._plotter.scene.set_background("black") + for widget in self._plotter._widgets: + widget._dark_mode = True + widget.update() + + # Using internal method to force the render to update + self._plotter.scene.iren._mouse_right_button_click() + else: + self._plotter.scene.set_background("white") + for widget in self._plotter._widgets: + widget._dark_mode = False + widget.update() + + # Using internal method to force the render to update + self._plotter.scene.iren._mouse_right_button_click() + + def update(self) -> None: + """Define the hide widget button parameters.""" + if self._dark_mode: + is_inv = "_inv" + else: + is_inv = "" + + show_vr = self._button.GetRepresentation() + show_vison_icon_file = Path( + Path(__file__).parent / "_images" / f"dark_mode{is_inv}.png" + ) + show_visoff_icon_file = Path( + Path(__file__).parent / "_images" / f"dark_mode{is_inv}.png" + ) + show_r_on = vtkPNGReader() + show_r_on.SetFileName(show_vison_icon_file) + show_r_on.Update() + image_on = show_r_on.GetOutput() + + show_r_off = vtkPNGReader() + show_r_off.SetFileName(show_visoff_icon_file) + show_r_off.Update() + image_off = show_r_off.GetOutput() + + + show_vr.SetButtonTexture(0, image_off) + show_vr.SetButtonTexture(1, image_on) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/displace_arrows.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/displace_arrows.py index e8b6d50e..59b1c4b4 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/displace_arrows.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/displace_arrows.py @@ -30,12 +30,12 @@ class CameraPanDirection(Enum): """Provides an enum with the available movement directions of the camera.""" - XUP = 0, "upxarrow", (5, 170) - XDOWN = 1, "downarrow", (5, 130) - YUP = 2, "upyarrow", (35, 170) - YDOWN = 3, "downarrow", (35, 130) - ZUP = 4, "upzarrow", (65, 170) - ZDOWN = 5, "downarrow", (65, 130) + XUP = 0, "upxarrow", (5, 230) + XDOWN = 1, "downarrow", (5, 190) + YUP = 2, "upyarrow", (35, 230) + YDOWN = 3, "downarrow", (35, 190) + ZUP = 4, "upzarrow", (65, 230) + ZDOWN = 5, "downarrow", (65, 190) class DisplacementArrow(Button): diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/measure.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/measure.py index db01d18a..89b359ae 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/measure.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/measure.py @@ -52,7 +52,7 @@ def __init__(self, plotter_helper: "Plotter", dark_mode: bool = False) -> None: self._actor: vtkActor = None self.plotter_helper = plotter_helper self._button: vtkButtonWidget = self.plotter_helper._pl.scene.add_checkbox_button_widget( - self.callback, position=(10, 60), size=30, border_size=3 + self.callback, position=(5, 160), size=30, border_size=3 ) self.update() diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/mesh_slider.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/mesh_slider.py index 11c26d1b..393a163b 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/mesh_slider.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/mesh_slider.py @@ -52,7 +52,7 @@ def __init__(self, plotter_helper: "Plotter", dark_mode: bool = False) -> None: self._widget_actor: vtkActor = None self.plotter_helper = plotter_helper self._button: vtkButtonWidget = self.plotter_helper._pl.scene.add_checkbox_button_widget( - self.callback, position=(45, 60), size=30, border_size=3 + self.callback, position=(37, 160), size=30, border_size=3 ) self._mb = None self._mesh_actor_list = [] diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/pick_rotation_center.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/pick_rotation_center.py index 30c6d5e6..44bee51d 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/pick_rotation_center.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/pick_rotation_center.py @@ -52,7 +52,7 @@ def __init__(self, plotter_helper: "Plotter", dark_mode: bool = False) -> None: self._actor: vtkActor = None self.plotter_helper = plotter_helper self._button: vtkButtonWidget = self.plotter_helper._pl.scene.add_checkbox_button_widget( - self.callback, position=(45, 10), size=30, border_size=3 + self.callback, position=(37, 128), size=30, border_size=3 ) self.update() diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/ruler.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/ruler.py index ea013974..81d6b47d 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/ruler.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/ruler.py @@ -49,7 +49,7 @@ def __init__(self, plotter: Plotter, dark_mode: bool = False) -> None: # Initialize variables self._actor: vtkActor = None self._button: vtkButtonWidget = self.plotter.add_checkbox_button_widget( - self.callback, position=(10, 100), size=30, border_size=3 + self.callback, position=(3, 128), size=30, border_size=3 ) self.update() diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/screenshot.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/screenshot.py index c23281aa..830ddfbc 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/screenshot.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/screenshot.py @@ -49,7 +49,7 @@ def __init__(self, plotter: Plotter, dark_mode: bool = False) -> None: # Initialize variables self._actor: vtkActor = None self._button: vtkButtonWidget = self.plotter._pl.scene.add_checkbox_button_widget( - self.callback, position=(45, 100), size=30, border_size=3 + self.callback, position=(69, 160), size=30, border_size=3 ) self.update() diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/view_button.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/view_button.py index a8e040c9..0e9ce663 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/view_button.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/view_button.py @@ -31,13 +31,13 @@ class ViewDirection(Enum): """Provides an enum with the available views.""" - XYPLUS = 0, "+xy", (5, 220) - XYMINUS = 1, "-xy", (5, 251) - XZPLUS = 2, "+xz", (5, 282) - XZMINUS = 3, "-xz", (5, 313) - YZPLUS = 4, "+yz", (5, 344) - YZMINUS = 5, "-yz", (5, 375) - ISOMETRIC = 6, "isometric", (5, 406) + XYPLUS = 0, "+xy", (5, 280) + XYMINUS = 1, "-xy", (5, 311) + XZPLUS = 2, "+xz", (5, 342) + XZMINUS = 3, "-xz", (5, 373) + YZPLUS = 4, "+yz", (5, 404) + YZMINUS = 5, "-yz", (5, 435) + ISOMETRIC = 6, "isometric", (5, 466) class ViewButton(Button): diff --git a/tests/test_interactables.py b/tests/test_interactables.py index c85d70d1..7c668d0a 100644 --- a/tests/test_interactables.py +++ b/tests/test_interactables.py @@ -70,13 +70,13 @@ def test_picking(): # TODO: View and displace arrows tests do not give expected results, PyVista issue? view = [ - (5, 220), # XYPLUS - (5, 251), # XYMINUS - (5, 282), # XZPLUS - (5, 313), # XZMINUS - (5, 344), # YZPLUS - (5, 375), # YZMINUS - (5, 406) # ISOMETRIC + (5, 280), # XYPLUS + (5, 311), # XYMINUS + (5, 342), # XZPLUS + (5, 373), # XZMINUS + (5, 404), # YZPLUS + (5, 435), # YZMINUS + (5, 466) # ISOMETRIC ] @@ -107,12 +107,12 @@ def test_view_buttons(view): raw_plotter.close() displace_arrow = [ - (5, 170), # XUP - (5, 130), # XDOWN - (35, 170), # YUP - (35, 130), # YDOWN - (65, 170), # ZUP - (65, 130) # ZDOWN + (5, 230), # XUP + (5, 190), # XDOWN + (35, 230), # YUP + (35, 190), # YDOWN + (65, 230), # ZUP + (65, 190) # ZDOWN ] @pytest.mark.parametrize("displace_arrow", displace_arrow) @@ -160,7 +160,7 @@ def test_ruler_button(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(10, 100) + raw_plotter.iren._mouse_left_button_click(3, 128) raw_plotter.close() @@ -184,7 +184,7 @@ def test_measure_tool(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(10, 60) + raw_plotter.iren._mouse_left_button_click(5, 160) raw_plotter.iren._mouse_left_button_click(200, 200) raw_plotter.iren._mouse_left_button_click(300, 300) raw_plotter.close() @@ -210,10 +210,10 @@ def test_measure_tool_close(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(10, 60) + raw_plotter.iren._mouse_left_button_click(5, 160) raw_plotter.iren._mouse_left_button_click(200, 200) raw_plotter.iren._mouse_left_button_click(300, 300) - raw_plotter.iren._mouse_left_button_click(10, 60) + raw_plotter.iren._mouse_left_button_click(5, 160) raw_plotter.close() @@ -238,7 +238,7 @@ def test_screenshot_button(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(45, 100) + raw_plotter.iren._mouse_left_button_click(69, 160) raw_plotter.close() assert Path("screenshot.png").exists() @@ -317,7 +317,7 @@ def test_slicing_tool(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(45, 60) + raw_plotter.iren._mouse_left_button_click(37, 160) raw_plotter.close() def test_pick_rotation_center(): @@ -340,7 +340,30 @@ def test_pick_rotation_center(): raw_plotter = pl.backend.scene width, height = raw_plotter.window_size - raw_plotter.iren._mouse_left_button_click(45, 10) + raw_plotter.iren._mouse_left_button_click(37, 128) raw_plotter.iren._mouse_right_button_press(width//2, height//2) raw_plotter.iren._mouse_right_button_release(width//2, height//2) + raw_plotter.close() + +def test_dark_mode_button(): + """Test dark mode button interaction.""" + pv_backend = PyVistaBackend() + + pl = Plotter(backend=pv_backend) + + # Create custom sphere + custom_sphere = CustomObject() + custom_sphere.mesh = pv.Sphere(center=(0, 0, 5)) + custom_sphere.name = "CustomSphere" + mesh_object_sphere = MeshObjectPlot(custom_sphere, custom_sphere.get_mesh()) + + pl.plot(mesh_object_sphere) + + # Run the plotter and simulate a click + pl.show(auto_close=False) + + raw_plotter = pl.backend.scene + width, height = raw_plotter.window_size + + raw_plotter.iren._mouse_left_button_click(43, 10) raw_plotter.close() \ No newline at end of file From 838c6cd831a52ad448b84a5dfd43d347ebed6ae9 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:42:03 +0000 Subject: [PATCH 2/3] chore: adding changelog file 359.maintenance.md [dependabot-skip] --- doc/changelog.d/359.maintenance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/359.maintenance.md diff --git a/doc/changelog.d/359.maintenance.md b/doc/changelog.d/359.maintenance.md new file mode 100644 index 00000000..fcdb6e4e --- /dev/null +++ b/doc/changelog.d/359.maintenance.md @@ -0,0 +1 @@ +Feat: Add night mode and reorganize buttons From 93cae1b02b6eb21b57c9eb27eef0647502eb45a6 Mon Sep 17 00:00:00 2001 From: Alex Fernandez Luces Date: Fri, 26 Sep 2025 15:49:49 +0200 Subject: [PATCH 3/3] fix: Docstrings --- .../backends/pyvista/widgets/dark_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py index 486b0407..766bd022 100644 --- a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py +++ b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/dark_mode.py @@ -19,7 +19,7 @@ # 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. -"""Provides the hide buttons widget for the PyAnsys plotter.""" +"""Provides the dark mode button widget for the PyAnsys plotter.""" from pathlib import Path from typing import TYPE_CHECKING @@ -83,7 +83,7 @@ def callback(self, state: bool) -> None: self._plotter.scene.iren._mouse_right_button_click() def update(self) -> None: - """Define the hide widget button parameters.""" + """Define the dark mode widget button parameters.""" if self._dark_mode: is_inv = "_inv" else: