Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/357.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feat: Add rotation center selection
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from ansys.tools.visualization_interface.backends.pyvista.widgets.mesh_slider import (
MeshSliderWidget,
)
from ansys.tools.visualization_interface.backends.pyvista.widgets.pick_rotation_center import PickRotCenterButton
from ansys.tools.visualization_interface.backends.pyvista.widgets.ruler import Ruler
from ansys.tools.visualization_interface.backends.pyvista.widgets.screenshot import ScreenshotButton
from ansys.tools.visualization_interface.backends.pyvista.widgets.view_button import (
Expand Down Expand Up @@ -207,6 +208,7 @@ def enable_widgets(self, dark_mode: bool = False) -> None:
if not self._use_qt:
self._widgets.append(MeshSliderWidget(self, dark_mode))
self._widgets.append(HideButton(self, dark_mode))
self._widgets.append(PickRotCenterButton(self, dark_mode))

def add_widget(self, widget: Union[PlotterWidget, List[PlotterWidget]]):
"""Add one or more custom widgets to the plotter.
Expand Down Expand Up @@ -363,6 +365,21 @@ def hover_callback(self, _widget, event_name) -> None:
for label in self._added_hover_labels:
self._pl.scene.remove_actor(label)

def focus_point_selection(self, actor: "pv.Actor") -> None:
"""Focus the camera on a selected actor.

Parameters
----------
actor : ~pyvista.Actor
Actor to focus the camera on.

"""
pt = self._pl.scene.picked_point
sphere = pv.Sphere(center=pt, radius=0.1)
self._picked_ball = self._pl.scene.add_mesh(sphere, color="red", name="focus_sphere_temp", reset_camera=False)
self._pl.scene.set_focus(pt)
self._pl.scene.render()

def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]:
"""Compute the mapping between plotter actors and ``EdgePlot`` objects.

Expand All @@ -388,6 +405,16 @@ def enable_picking(self):
picker="cell",
)

def enable_set_focus_center(self):
"""Enable setting the focus of the camera to the picked point."""
self._pl.scene.enable_mesh_picking(
callback=self.focus_point_selection,
use_actor=True,
show=False,
show_message=False,
picker="cell",
)

def enable_hover(self):
"""Enable hover capabilities in the plotter."""
self._hover_widget = vtkHoverWidget()
Expand All @@ -406,6 +433,11 @@ def disable_hover(self):
"""Disable hover capabilities in the plotter."""
self._hover_widget.EnabledOff()

def disable_center_focus(self):
"""Disable setting the focus of the camera to the picked point."""
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.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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 measure 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 PickRotCenterButton(PlotterWidget):
"""Provides the pick rotation center widget for the Visualization Interface Tool ``Plotter`` class.

Parameters
----------
plotter_helper : PlotterHelper
Plotter to add the pick rotation center widget to.
dark_mode : bool, optional
Whether to activate the dark mode or not.

"""

def __init__(self, plotter_helper: "Plotter", dark_mode: bool = False) -> None:
"""Initialize the ``PickRotCenterWidget`` class."""
# Call PlotterWidget ctor
super().__init__(plotter_helper._pl.scene)
self._dark_mode = dark_mode
# Initialize variables
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.update()

def callback(self, state: bool) -> None:
"""Remove or add the pick rotation center widget actor upon click.

Parameters
----------
state : bool
Whether the state of the button, which is inherited from PyVista, is active.

"""
# This implementation uses direct calls to VTK due to limitations
# in PyVista. If there are improvements in the compatibility between
# the PyVista picker and the pick rotation center widget, this should be reviewed.

# Lazy import to avoid circular import
if not state:
self._text_actor.SetVisibility(0)
self.plotter_helper.disable_center_focus()
if self.plotter_helper._allow_picking:
self.plotter_helper.enable_picking()
elif self.plotter_helper._allow_hovering:
self.plotter_helper.enable_hover()
else:
if self.plotter_helper._allow_picking:
self.plotter_helper.disable_picking()
elif self.plotter_helper._allow_hovering:
self.plotter_helper.disable_hover()
self.plotter_helper.enable_set_focus_center()
self._text_actor = self.plotter_helper.scene.add_text(
"Select a point to set the rotation center with right click",
position="upper_edge", # options: 'upper_edge', 'lower_edge', 'left_edge', etc.
font_size=14,
color="grey",
shadow=True
)


def update(self) -> None:
"""Define the measurement widget button parameters."""
if self._dark_mode:
is_inv = "_inv"
else:
is_inv = ""
show_measure_vr = self._button.GetRepresentation()
show_measure_icon_file = Path(
Path(__file__).parent / "_images" / f"center_pick{is_inv}.png"
)
show_measure_r = vtkPNGReader()
show_measure_r.SetFileName(show_measure_icon_file)
show_measure_r.Update()
image = show_measure_r.GetOutput()
show_measure_vr.SetButtonTexture(0, image)
show_measure_vr.SetButtonTexture(1, image)
25 changes: 25 additions & 0 deletions tests/test_interactables.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,29 @@ def test_slicing_tool():
width, height = raw_plotter.window_size

raw_plotter.iren._mouse_left_button_click(45, 60)
raw_plotter.close()

def test_pick_rotation_center():
"""Test pick rotation center tool 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(45, 10)
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()
Loading