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/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 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 00000000..64626f75 Binary files /dev/null and b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode.png differ diff --git a/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode_inv.png b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode_inv.png new file mode 100644 index 00000000..58fd2416 Binary files /dev/null and b/src/ansys/tools/visualization_interface/backends/pyvista/widgets/_images/dark_mode_inv.png differ 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..766bd022 --- /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 dark mode button 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 dark mode 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