Skip to content

Commit 29d7f9d

Browse files
AlejandroFernandezLucespre-commit-ci[bot]pyansys-ci-bot
authored
feat: Add rotation center selection (#357)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent f655182 commit 29d7f9d

File tree

6 files changed

+168
-0
lines changed

6 files changed

+168
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Feat: Add rotation center selection

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from ansys.tools.visualization_interface.backends.pyvista.widgets.mesh_slider import (
4343
MeshSliderWidget,
4444
)
45+
from ansys.tools.visualization_interface.backends.pyvista.widgets.pick_rotation_center import PickRotCenterButton
4546
from ansys.tools.visualization_interface.backends.pyvista.widgets.ruler import Ruler
4647
from ansys.tools.visualization_interface.backends.pyvista.widgets.screenshot import ScreenshotButton
4748
from ansys.tools.visualization_interface.backends.pyvista.widgets.view_button import (
@@ -207,6 +208,7 @@ def enable_widgets(self, dark_mode: bool = False) -> None:
207208
if not self._use_qt:
208209
self._widgets.append(MeshSliderWidget(self, dark_mode))
209210
self._widgets.append(HideButton(self, dark_mode))
211+
self._widgets.append(PickRotCenterButton(self, dark_mode))
210212

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

368+
def focus_point_selection(self, actor: "pv.Actor") -> None:
369+
"""Focus the camera on a selected actor.
370+
371+
Parameters
372+
----------
373+
actor : ~pyvista.Actor
374+
Actor to focus the camera on.
375+
376+
"""
377+
pt = self._pl.scene.picked_point
378+
sphere = pv.Sphere(center=pt, radius=0.1)
379+
self._picked_ball = self._pl.scene.add_mesh(sphere, color="red", name="focus_sphere_temp", reset_camera=False)
380+
self._pl.scene.set_focus(pt)
381+
self._pl.scene.render()
382+
366383
def compute_edge_object_map(self) -> Dict[pv.Actor, EdgePlot]:
367384
"""Compute the mapping between plotter actors and ``EdgePlot`` objects.
368385
@@ -388,6 +405,16 @@ def enable_picking(self):
388405
picker="cell",
389406
)
390407

408+
def enable_set_focus_center(self):
409+
"""Enable setting the focus of the camera to the picked point."""
410+
self._pl.scene.enable_mesh_picking(
411+
callback=self.focus_point_selection,
412+
use_actor=True,
413+
show=False,
414+
show_message=False,
415+
picker="cell",
416+
)
417+
391418
def enable_hover(self):
392419
"""Enable hover capabilities in the plotter."""
393420
self._hover_widget = vtkHoverWidget()
@@ -406,6 +433,11 @@ def disable_hover(self):
406433
"""Disable hover capabilities in the plotter."""
407434
self._hover_widget.EnabledOff()
408435

436+
def disable_center_focus(self):
437+
"""Disable setting the focus of the camera to the picked point."""
438+
self._pl.scene.disable_picking()
439+
self._picked_ball.SetVisibility(False)
440+
409441
def __extract_kwargs(self, func_name: Callable, input_kwargs: Dict[str, Any]) -> Dict[str, Any]:
410442
"""Extracts the keyword arguments from a function signature and returns it as dict.
411443
640 Bytes
Loading
575 Bytes
Loading
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright (C) 2024 - 2025 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+
"""Provides the measure widget for the PyAnsys plotter."""
23+
from pathlib import Path
24+
from typing import TYPE_CHECKING
25+
26+
from vtk import vtkActor, vtkButtonWidget, vtkPNGReader
27+
28+
from ansys.tools.visualization_interface.backends.pyvista.widgets.widget import PlotterWidget
29+
30+
if TYPE_CHECKING:
31+
from ansys.tools.visualization_interface.backends.pyvista.pyvista import Plotter
32+
33+
34+
class PickRotCenterButton(PlotterWidget):
35+
"""Provides the pick rotation center widget for the Visualization Interface Tool ``Plotter`` class.
36+
37+
Parameters
38+
----------
39+
plotter_helper : PlotterHelper
40+
Plotter to add the pick rotation center widget to.
41+
dark_mode : bool, optional
42+
Whether to activate the dark mode or not.
43+
44+
"""
45+
46+
def __init__(self, plotter_helper: "Plotter", dark_mode: bool = False) -> None:
47+
"""Initialize the ``PickRotCenterWidget`` class."""
48+
# Call PlotterWidget ctor
49+
super().__init__(plotter_helper._pl.scene)
50+
self._dark_mode = dark_mode
51+
# Initialize variables
52+
self._actor: vtkActor = None
53+
self.plotter_helper = plotter_helper
54+
self._button: vtkButtonWidget = self.plotter_helper._pl.scene.add_checkbox_button_widget(
55+
self.callback, position=(45, 10), size=30, border_size=3
56+
)
57+
self.update()
58+
59+
def callback(self, state: bool) -> None:
60+
"""Remove or add the pick rotation center widget actor upon click.
61+
62+
Parameters
63+
----------
64+
state : bool
65+
Whether the state of the button, which is inherited from PyVista, is active.
66+
67+
"""
68+
# This implementation uses direct calls to VTK due to limitations
69+
# in PyVista. If there are improvements in the compatibility between
70+
# the PyVista picker and the pick rotation center widget, this should be reviewed.
71+
72+
# Lazy import to avoid circular import
73+
if not state:
74+
self._text_actor.SetVisibility(0)
75+
self.plotter_helper.disable_center_focus()
76+
if self.plotter_helper._allow_picking:
77+
self.plotter_helper.enable_picking()
78+
elif self.plotter_helper._allow_hovering:
79+
self.plotter_helper.enable_hover()
80+
else:
81+
if self.plotter_helper._allow_picking:
82+
self.plotter_helper.disable_picking()
83+
elif self.plotter_helper._allow_hovering:
84+
self.plotter_helper.disable_hover()
85+
self.plotter_helper.enable_set_focus_center()
86+
self._text_actor = self.plotter_helper.scene.add_text(
87+
"Select a point to set the rotation center with right click",
88+
position="upper_edge", # options: 'upper_edge', 'lower_edge', 'left_edge', etc.
89+
font_size=14,
90+
color="grey",
91+
shadow=True
92+
)
93+
94+
95+
def update(self) -> None:
96+
"""Define the measurement widget button parameters."""
97+
if self._dark_mode:
98+
is_inv = "_inv"
99+
else:
100+
is_inv = ""
101+
show_measure_vr = self._button.GetRepresentation()
102+
show_measure_icon_file = Path(
103+
Path(__file__).parent / "_images" / f"center_pick{is_inv}.png"
104+
)
105+
show_measure_r = vtkPNGReader()
106+
show_measure_r.SetFileName(show_measure_icon_file)
107+
show_measure_r.Update()
108+
image = show_measure_r.GetOutput()
109+
show_measure_vr.SetButtonTexture(0, image)
110+
show_measure_vr.SetButtonTexture(1, image)

tests/test_interactables.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,29 @@ def test_slicing_tool():
318318
width, height = raw_plotter.window_size
319319

320320
raw_plotter.iren._mouse_left_button_click(45, 60)
321+
raw_plotter.close()
322+
323+
def test_pick_rotation_center():
324+
"""Test pick rotation center tool interaction."""
325+
pv_backend = PyVistaBackend()
326+
327+
pl = Plotter(backend=pv_backend)
328+
329+
# Create custom sphere
330+
custom_sphere = CustomObject()
331+
custom_sphere.mesh = pv.Sphere(center=(0, 0, 5))
332+
custom_sphere.name = "CustomSphere"
333+
mesh_object_sphere = MeshObjectPlot(custom_sphere, custom_sphere.get_mesh())
334+
335+
pl.plot(mesh_object_sphere)
336+
337+
# Run the plotter and simulate a click
338+
pl.show(auto_close=False)
339+
340+
raw_plotter = pl.backend.scene
341+
width, height = raw_plotter.window_size
342+
343+
raw_plotter.iren._mouse_left_button_click(45, 10)
344+
raw_plotter.iren._mouse_right_button_press(width//2, height//2)
345+
raw_plotter.iren._mouse_right_button_release(width//2, height//2)
321346
raw_plotter.close()

0 commit comments

Comments
 (0)