283 lines
9.8 KiB
Python
283 lines
9.8 KiB
Python
"""Trame view interface for PyVista."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import weakref
|
|
|
|
from trame.app import get_server as trame_get_server
|
|
from trame.widgets.vtk import VtkLocalView
|
|
from trame.widgets.vtk import VtkRemoteLocalView
|
|
from trame.widgets.vtk import VtkRemoteView
|
|
from trame_vtk.tools.vtksz2html import write_html
|
|
|
|
CLOSED_PLOTTER_ERROR = (
|
|
'The render window for this plotter has been destroyed. '
|
|
'Do not call `show()` for the plotter before passing to trame.'
|
|
)
|
|
|
|
|
|
def get_server(*args, **kwargs): # numpydoc ignore=RT01
|
|
"""Override trame's get_server.
|
|
|
|
Parameters
|
|
----------
|
|
*args :
|
|
Any extra args are passed as option to the server instance.
|
|
|
|
**kwargs :
|
|
Any extra keyword args are passed as option to the server instance.
|
|
|
|
Returns
|
|
-------
|
|
trame_server.core.Server
|
|
Trame server.
|
|
|
|
"""
|
|
server = trame_get_server(*args, **kwargs)
|
|
if 'client_type' in kwargs:
|
|
server.client_type = kwargs['client_type']
|
|
return server
|
|
|
|
|
|
class _BasePyVistaView:
|
|
def __init__(self, plotter):
|
|
"""Initialize the base PyVista view."""
|
|
self._plotter = weakref.ref(plotter)
|
|
self.pyvista_initialize()
|
|
self._plotter_render_callback = lambda *_: self.update() # type: ignore[attr-defined]
|
|
|
|
def pyvista_initialize(self):
|
|
if self._plotter().render_window is None: # type: ignore[union-attr]
|
|
raise RuntimeError(CLOSED_PLOTTER_ERROR)
|
|
for renderer in self._plotter().renderers: # type: ignore[union-attr]
|
|
if not renderer.camera.is_set:
|
|
renderer.camera_position = renderer.get_default_cam_pos()
|
|
renderer.ResetCamera()
|
|
|
|
def _post_initialize(self):
|
|
if self._server.running: # type: ignore[attr-defined]
|
|
self.update() # type: ignore[attr-defined]
|
|
else:
|
|
self._server.controller.on_server_ready.add(self.update) # type: ignore[attr-defined]
|
|
|
|
# Callback to sync view on PyVista's render call when renders are suppressed
|
|
self._plotter().add_on_render_callback(self._plotter_render_callback, render_event=False) # type: ignore[union-attr]
|
|
|
|
def update_camera(self):
|
|
"""Update camera or push the image."""
|
|
self.push_camera() # type: ignore[attr-defined]
|
|
self.update_image() # type: ignore[attr-defined]
|
|
|
|
def export_html(self):
|
|
"""Export scene to HTML as StringIO buffer."""
|
|
content = io.StringIO()
|
|
if isinstance(self, PyVistaLocalView):
|
|
data = self.export(format='zip')
|
|
if data is None:
|
|
msg = 'No data to write.'
|
|
raise ValueError(msg)
|
|
write_html(data, content)
|
|
content.seek(0)
|
|
elif isinstance(self, PyVistaRemoteLocalView):
|
|
data = self.export_geometry(format='zip')
|
|
if data is None:
|
|
msg = 'No data to write.'
|
|
raise ValueError(msg)
|
|
write_html(data, content)
|
|
content.seek(0)
|
|
else:
|
|
content = self._plotter().export_html(filename=None) # type: ignore[union-attr]
|
|
return io.BytesIO(content.read().encode('utf8')).read()
|
|
|
|
|
|
class PyVistaRemoteView(VtkRemoteView, _BasePyVistaView): # type: ignore[misc]
|
|
"""PyVista wrapping of trame ``VtkRemoteView`` for server rendering.
|
|
|
|
This will connect to a PyVista plotter and stream the server-side
|
|
renderings.
|
|
|
|
Parameters
|
|
----------
|
|
plotter : pyvista.Plotter
|
|
The PyVista Plotter to display in the output view.
|
|
|
|
interactive_ratio : int, optional
|
|
Image size scale factor while interacting. Increasing this
|
|
value will give higher resulotuion images during interaction
|
|
events at the cost of performance. Use lower values (e.g.,
|
|
``0.5``) to increase performance while interacting.
|
|
Defaults to 1.
|
|
|
|
still_ratio : int, optional
|
|
Image size scale factor while not interacting (still).
|
|
Increasing this value will give higher resulotuion images
|
|
when not interacting with the scene. Defaults to 1.
|
|
|
|
namespace : str, optional
|
|
The namespace for this view component. A default value is
|
|
chosen based on the ``_id_name`` of the plotter.
|
|
|
|
**kwargs : dict, optional
|
|
Any additional keyword arguments to pass to
|
|
``trame.widgets.vtk.VtkRemoteView``.
|
|
|
|
Notes
|
|
-----
|
|
For optimal rendering results, you may want to have the same
|
|
value for ``interactive_ratio`` and ``still_ratio`` so that
|
|
the entire rendering is not re-scaled between interaction events.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
plotter,
|
|
interactive_ratio=None,
|
|
still_ratio=None,
|
|
namespace=None,
|
|
**kwargs,
|
|
): # numpydoc ignore=PR01,RT01
|
|
"""Create a trame remote view from a PyVista Plotter."""
|
|
_BasePyVistaView.__init__(self, plotter)
|
|
if namespace is None:
|
|
namespace = f'{plotter._id_name}'
|
|
if interactive_ratio is None:
|
|
interactive_ratio = plotter._theme.trame.interactive_ratio
|
|
if still_ratio is None:
|
|
still_ratio = plotter._theme.trame.still_ratio
|
|
VtkRemoteView.__init__(
|
|
self,
|
|
self._plotter().render_window, # type: ignore[union-attr]
|
|
interactive_ratio=interactive_ratio,
|
|
still_ratio=still_ratio,
|
|
__properties=[('still_ratio', 'stillRatio')],
|
|
ref=f'view_{plotter._id_name}',
|
|
namespace=namespace,
|
|
**kwargs,
|
|
)
|
|
self._post_initialize()
|
|
|
|
def push_camera(self, *args, **kwargs): # pragma: no cover
|
|
"""No-op implementation to match local viewers."""
|
|
|
|
def update_image(self, *args, **kwargs): # numpydoc ignore=RT01
|
|
"""Wrap update call."""
|
|
return self.update(*args, **kwargs)
|
|
|
|
|
|
class PyVistaLocalView(VtkLocalView, _BasePyVistaView): # type: ignore[misc]
|
|
"""PyVista wrapping of trame VtkLocalView for in-browser rendering.
|
|
|
|
This will connect to and synchronize with a PyVista plotter to
|
|
perform client-side rendering with VTK.js in the browser.
|
|
|
|
Parameters
|
|
----------
|
|
plotter : pyvista.Plotter
|
|
The PyVista Plotter to represent in the output view.
|
|
|
|
namespace : str, optional
|
|
The namespace for this view component. A default value is
|
|
chosen based on the ``_id_name`` of the plotter.
|
|
|
|
**kwargs : dict, optional
|
|
Any additional keyword arguments to pass to
|
|
``trame.widgets.vtk.VtkLocalView``.
|
|
|
|
"""
|
|
|
|
def __init__(self, plotter, namespace=None, **kwargs):
|
|
"""Create a trame local view from a PyVista Plotter."""
|
|
_BasePyVistaView.__init__(self, plotter)
|
|
if namespace is None:
|
|
namespace = f'{plotter._id_name}'
|
|
VtkLocalView.__init__(
|
|
self,
|
|
self._plotter().render_window, # type: ignore[union-attr]
|
|
ref=f'view_{plotter._id_name}',
|
|
namespace=namespace,
|
|
**kwargs,
|
|
)
|
|
self._post_initialize()
|
|
|
|
def _post_initialize(self):
|
|
super()._post_initialize()
|
|
self.set_widgets(
|
|
[ren.axes_widget for ren in self._plotter().renderers if ren.axes_widget is not None], # type: ignore[union-attr]
|
|
)
|
|
|
|
def update_image(self, *args, **kwargs): # pragma: no cover
|
|
"""No-op implementation to match remote viewers."""
|
|
|
|
|
|
class PyVistaRemoteLocalView(VtkRemoteLocalView, _BasePyVistaView): # type: ignore[misc]
|
|
"""PyVista wrapping of trame ``VtkRemoteLocalView``.
|
|
|
|
Dynamically switch between client and server rendering.
|
|
|
|
Parameters
|
|
----------
|
|
plotter : pyvista.Plotter
|
|
The PyVista Plotter to display in the output view.
|
|
|
|
interactive_ratio : int, optional
|
|
Image size scale factor while interacting. Increasing this
|
|
value will give higher resulotuion images during interaction
|
|
events at the cost of performance. Use lower values (e.g.,
|
|
``0.5``) to increase performance while interacting.
|
|
Defaults to 1. This is only valid in the ``'remote'`` mode.
|
|
|
|
still_ratio : int, optional
|
|
Image size scale factor while not interacting (still).
|
|
Increasing this value will give higher resulotuion images
|
|
when not interacting with the scene. Defaults to 1.
|
|
This is only valid in the ``'remote'`` mode.
|
|
|
|
namespace : str, optional
|
|
The namespace for this view component. A default value is
|
|
chosen based on the ``_id_name`` of the plotter.
|
|
|
|
**kwargs : dict, optional
|
|
Any additional keyword arguments to pass to
|
|
``trame.widgets.vtk.VtkRemoteLocalView``.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
plotter,
|
|
interactive_ratio=None,
|
|
still_ratio=None,
|
|
namespace=None,
|
|
**kwargs,
|
|
): # numpydoc ignore=PR01,RT01
|
|
"""Create a trame remote/local view from a PyVista Plotter."""
|
|
_BasePyVistaView.__init__(self, plotter)
|
|
if namespace is None:
|
|
namespace = f'{plotter._id_name}'
|
|
if interactive_ratio is None:
|
|
interactive_ratio = plotter._theme.trame.interactive_ratio
|
|
if still_ratio is None:
|
|
still_ratio = plotter._theme.trame.still_ratio
|
|
VtkRemoteLocalView.__init__(
|
|
self,
|
|
self._plotter().render_window, # type: ignore[union-attr]
|
|
interactive_ratio=interactive_ratio,
|
|
still_ratio=still_ratio,
|
|
__properties=[('still_ratio', 'stillRatio')],
|
|
ref=f'view_{plotter._id_name}',
|
|
namespace=namespace,
|
|
**kwargs,
|
|
)
|
|
# Track namespace for our use since trame attributes are name mangled
|
|
self._namespace = namespace
|
|
|
|
self._post_initialize()
|
|
|
|
def _post_initialize(self):
|
|
super()._post_initialize()
|
|
self.set_widgets(
|
|
[ren.axes_widget for ren in self._plotter().renderers if ren.axes_widget is not None], # type: ignore[union-attr]
|
|
)
|