diff --git a/examples/validation/Animation.py b/examples/validation/Animation.py new file mode 100644 index 0000000..3ad4bdd --- /dev/null +++ b/examples/validation/Animation.py @@ -0,0 +1,99 @@ +import asyncio + +from trame.app import get_server, asynchronous +from trame.ui.vuetify3 import SinglePageLayout +from trame.widgets import vuetify3 as v3, vtk as vtk_widgets + +from vtkmodules.vtkFiltersSources import vtkConeSource +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderer, + vtkRenderWindow, + vtkRenderWindowInteractor, +) +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa +import vtkmodules.vtkRenderingOpenGL2 # noqa + +renderer = vtkRenderer() +renderWindow = vtkRenderWindow() +renderWindow.AddRenderer(renderer) + +renderWindowInteractor = vtkRenderWindowInteractor() +renderWindowInteractor.SetRenderWindow(renderWindow) +renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() + +cone_source = vtkConeSource() +mapper = vtkPolyDataMapper() +mapper.SetInputConnection(cone_source.GetOutputPort()) +actor = vtkActor() +actor.SetMapper(mapper) +renderer.AddActor(actor) +renderer.ResetCamera() + +server = get_server(client_type="vue3") +state, ctrl = server.state, server.controller + + +async def animate(): + while True: + await asyncio.sleep(1.0 / state.anime_rate) + if state.mode == "animate": + renderer.GetActiveCamera().Azimuth(1) + if state.mode == "update": + renderer.GetActiveCamera().Azimuth(1) + ctrl.view_update() + + +@state.change("mode") +def on_animation_change(mode, **_): + if mode == "animate": + ctrl.view_start_animation(state.fps, 60, 1) + else: + ctrl.view_stop_animation() + + +asynchronous.create_task(animate()) + +with SinglePageLayout(server) as layout: + layout.title.set_text("Anim({{ anime_rate }}) Push Rate {{ fps }}") + + with layout.toolbar: + v3.VSelect( + v_model=("mode", "stop"), + items=("modes", ["stop", "animate", "update"]), + hide_details=True, + ) + v3.VSlider( + v_model=("fps", 30), + min=10, + max=120, + step=5, + hide_details=True, + classes="mx-4", + style="max-width: 200px;", + ) + v3.VSlider( + v_model=("anime_rate", 30), + min=10, + max=120, + step=5, + hide_details=True, + classes="mx-4", + style="max-width: 200px;", + ) + + with layout.content: + with v3.VContainer( + fluid=True, + classes="pa-0 fill-height", + ): + view = vtk_widgets.VtkRemoteView( + renderWindow, interactive_quality=80, interactive_ratio=1 + ) + ctrl.view_update = view.update + ctrl.view_start_animation = view.start_animation + ctrl.view_stop_animation = view.stop_animation + +if __name__ == "__main__": + server.start() diff --git a/examples/validation/AnimationPV.py b/examples/validation/AnimationPV.py new file mode 100644 index 0000000..6f361b5 --- /dev/null +++ b/examples/validation/AnimationPV.py @@ -0,0 +1,84 @@ +import asyncio + +from trame.app import get_server, asynchronous +from trame.ui.vuetify3 import SinglePageLayout +from trame.widgets import vuetify3 as v3, paraview as pv_widgets + +from paraview import simple + +server = get_server(client_type="vue3") +state, ctrl = server.state, server.controller + +c = simple.Cone() +r = simple.Show() +v = simple.Render() + + +async def animate(): + while True: + await asyncio.sleep(1.0 / state.anime_rate) + if state.mode == "animate": + camera = v.GetActiveCamera() + camera.Azimuth(1) + pos = camera.GetPosition() + v.CameraPosition = pos + if state.mode == "update": + camera = v.GetActiveCamera() + camera.Azimuth(1) + pos = camera.GetPosition() + v.CameraPosition = pos + ctrl.view_update() + + +@state.change("mode") +def on_animation_change(mode, **_): + if mode == "animate": + ctrl.view_start_animation(state.fps, 60, 1) + else: + ctrl.view_stop_animation() + + +asynchronous.create_task(animate()) + +with SinglePageLayout(server) as layout: + layout.title.set_text("Anim({{ anime_rate }}) Push Rate {{ fps }}") + + with layout.toolbar: + v3.VSelect( + v_model=("mode", "stop"), + items=("modes", ["stop", "animate", "update"]), + hide_details=True, + ) + v3.VSlider( + v_model=("fps", 30), + min=10, + max=120, + step=5, + hide_details=True, + classes="mx-4", + style="max-width: 200px;", + ) + v3.VSlider( + v_model=("anime_rate", 30), + min=10, + max=120, + step=5, + hide_details=True, + classes="mx-4", + style="max-width: 200px;", + ) + + with layout.content: + with v3.VContainer( + fluid=True, + classes="pa-0 fill-height", + ): + view = pv_widgets.VtkRemoteView( + v, interactive_quality=80, interactive_ratio=1 + ) + ctrl.view_update = view.update + ctrl.view_start_animation = view.start_animation + ctrl.view_stop_animation = view.stop_animation + +if __name__ == "__main__": + server.start() diff --git a/trame_vtk/modules/paraview/__init__.py b/trame_vtk/modules/paraview/__init__.py index 9faac10..232c996 100644 --- a/trame_vtk/modules/paraview/__init__.py +++ b/trame_vtk/modules/paraview/__init__.py @@ -110,6 +110,31 @@ def push_image(self, view_proxy, reset_camera=False): "viewport.image.push", {"view": self.id(view_proxy)} ) + def get_current_image_quality(self, render_window): + return self._trame_server.protocol_call( + "viewport.image.push.quality.get", self.id(render_window) + ) + + def set_image_quality(self, render_window, quality, ratio): + self._trame_server.protocol_call( + "viewport.image.push.quality", + self.id(render_window), + quality, + ratio, + ) + + def start_animation(self, render_window, fps=30, quality=100, ratio=1): + self._trame_server.protocol_call("viewport.image.animation.fps.max", fps) + self.set_image_quality(render_window, quality, ratio) + self._trame_server.protocol_call( + "viewport.image.animation.start", self.id(render_window) + ) + + def stop_animation(self, render_window): + self._trame_server.protocol_call( + "viewport.image.animation.stop", self.id(render_window) + ) + def camera(self, view_proxy): view_proxy.UpdatePropertyInformation() return { diff --git a/trame_vtk/modules/paraview/protocols/publish_image_delivery.py b/trame_vtk/modules/paraview/protocols/publish_image_delivery.py index 3751d82..edf2454 100644 --- a/trame_vtk/modules/paraview/protocols/publish_image_delivery.py +++ b/trame_vtk/modules/paraview/protocols/publish_image_delivery.py @@ -396,6 +396,20 @@ def remove_render_observer(self, view_id): return {"result": "success"} + @export_rpc("viewport.image.push.quality.get") + def get_view_quality(self, view_id): + response = dict(quality=1, ratio=1) + s_view = self.get_view(view_id) + + if s_view: + real_view_id = s_view.GetGlobalIDAsString() + if real_view_id in self.tracking_views: + observer_info = self.tracking_views[real_view_id] + response["quality"] = observer_info.get("quality", 100) + response["ratio"] = observer_info.get("ratio", 1) + + return response + @export_rpc("viewport.image.push.quality") def set_view_quality(self, view_id, quality, ratio=1, update_linked_view=True): s_view = self.get_view(view_id) diff --git a/trame_vtk/modules/vtk/__init__.py b/trame_vtk/modules/vtk/__init__.py index 608b96f..8df1abe 100644 --- a/trame_vtk/modules/vtk/__init__.py +++ b/trame_vtk/modules/vtk/__init__.py @@ -107,6 +107,31 @@ def push_image(self, render_window, reset_camera=False): "viewport.image.push", {"view": self.id(render_window)} ) + def get_current_image_quality(self, render_window): + return self._trame_server.protocol_call( + "viewport.image.push.quality.get", self.id(render_window) + ) + + def set_image_quality(self, render_window, quality, ratio): + self._trame_server.protocol_call( + "viewport.image.push.quality", + self.id(render_window), + quality, + ratio, + ) + + def start_animation(self, render_window, fps=30, quality=100, ratio=1): + self._trame_server.protocol_call("viewport.image.animation.fps.max", fps) + self.set_image_quality(render_window, quality, ratio) + self._trame_server.protocol_call( + "viewport.image.animation.start", self.id(render_window) + ) + + def stop_animation(self, render_window): + self._trame_server.protocol_call( + "viewport.image.animation.stop", self.id(render_window) + ) + def camera(self, render_window): camera = render_window.GetRenderers().GetFirstRenderer().GetActiveCamera() return { diff --git a/trame_vtk/modules/vtk/protocols/publish_image_delivery.py b/trame_vtk/modules/vtk/protocols/publish_image_delivery.py index 81f95da..a10962a 100644 --- a/trame_vtk/modules/vtk/protocols/publish_image_delivery.py +++ b/trame_vtk/modules/vtk/protocols/publish_image_delivery.py @@ -93,6 +93,7 @@ def animate(self): next_animate_time = time.time() + 1.0 / self.target_frame_rate for v_id in self.views_in_animations: + self.tracking_views[v_id]["mtime"] = 0 self.push_render(v_id, True) next_animate_time -= time.time() @@ -273,6 +274,20 @@ def remove_render_observer(self, view_id): return {"result": "success"} + @export_rpc("viewport.image.push.quality.get") + def get_view_quality(self, view_id): + response = dict(quality=1, ratio=1) + s_view = self.get_view(view_id) + + if s_view: + real_view_id = str(self.get_global_id(s_view)) + if real_view_id in self.tracking_views: + observer_info = self.tracking_views[real_view_id] + response["quality"] = observer_info.get("quality", 100) + response["ratio"] = observer_info.get("ratio", 1) + + return response + @export_rpc("viewport.image.push.quality") def set_view_quality(self, view_id, quality, ratio=1): s_view = self.get_view(view_id) diff --git a/trame_vtk/widgets/vtk/common.py b/trame_vtk/widgets/vtk/common.py index 6ce114f..fe25045 100644 --- a/trame_vtk/widgets/vtk/common.py +++ b/trame_vtk/widgets/vtk/common.py @@ -677,6 +677,8 @@ class VtkRemoteView(HtmlElement): def __init__(self, view, ref=None, **kwargs): super().__init__("vtk-remote-view", **kwargs) + self._is_animating = False + self._img_quality = None self._helper = activate_module_for(None, self.server, view) self._helper.has_capabilities("web", "rendering") @@ -752,6 +754,23 @@ def update(self, **kwargs): """ self._helper.push_image(self.__view) + def start_animation(self, fps=30, quality=100, ratio=1): + if self._is_animating: + return + + self._is_animating = True + self._img_quality = self._helper.get_current_image_quality(self.__view) + self._helper.start_animation(self.__view, fps, quality, ratio) + + def stop_animation(self): + if not self._is_animating: + return + + self._is_animating = False + self._helper.set_image_quality(self.__view, **self._img_quality) + self._helper.stop_animation(self.__view) + self.update() + def reset_camera(self, **kwargs): self.server.js_call(ref=self.__ref, method="resetCamera")