init
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""Trame interface for PyVista."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
logging.getLogger('trame.app').disabled = True
|
||||
|
||||
from pyvista.trame.jupyter import elegantly_launch
|
||||
from pyvista.trame.jupyter import launch_server
|
||||
from pyvista.trame.jupyter import show_trame
|
||||
from pyvista.trame.ui import get_viewer
|
||||
from pyvista.trame.ui import plotter_ui
|
||||
from pyvista.trame.views import PyVistaLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteView
|
||||
|
||||
# __all__ only left for mypy --strict to work when pyvista is a dependency
|
||||
__all__ = [
|
||||
'PyVistaLocalView',
|
||||
'PyVistaRemoteLocalView',
|
||||
'PyVistaRemoteView',
|
||||
'elegantly_launch',
|
||||
'get_viewer',
|
||||
'launch_server',
|
||||
'plotter_ui',
|
||||
'show_trame',
|
||||
]
|
||||
@@ -0,0 +1,485 @@
|
||||
"""Trame utilities for running in Jupyter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Literal
|
||||
import warnings
|
||||
|
||||
from trame.widgets import html as html_widgets
|
||||
from trame.widgets import vtk as vtk_widgets
|
||||
from trame.widgets import vuetify as vuetify2_widgets
|
||||
from trame.widgets import vuetify3 as vuetify3_widgets
|
||||
from typing_extensions import Concatenate
|
||||
|
||||
try:
|
||||
from ipywidgets.widgets import HTML
|
||||
except ImportError:
|
||||
HTML = object
|
||||
|
||||
|
||||
import pyvista
|
||||
from pyvista.trame.ui import UI_TITLE
|
||||
from pyvista.trame.ui import get_viewer
|
||||
from pyvista.trame.views import CLOSED_PLOTTER_ERROR
|
||||
from pyvista.trame.views import get_server
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from IPython.display import IFrame
|
||||
|
||||
from pyvista.jupyter import JupyterBackendOptions
|
||||
from pyvista.plotting.plotter import Plotter
|
||||
from pyvista.trame.ui.vuetify2 import Viewer
|
||||
|
||||
SERVER_DOWN_MESSAGE = """Trame server has not launched.
|
||||
|
||||
You must start the trame server before attempting to `show()`
|
||||
with PyVista.
|
||||
|
||||
You can use the following snippet to launch the server:
|
||||
|
||||
from pyvista.trame.jupyter import launch_server
|
||||
await launch_server('{name}').ready
|
||||
|
||||
"""
|
||||
JUPYTER_SERVER_DOWN_MESSAGE = """Trame server has not launched.
|
||||
|
||||
Prior to plotting, please make sure to run `set_jupyter_backend('trame')` when using the
|
||||
`'trame'`, `'server'`, or `'client'` Jupyter backends.
|
||||
|
||||
import pyvista as pv
|
||||
pyvista.set_jupyter_backend('trame')
|
||||
|
||||
If this issue persists, please open an issue in PyVista: https://github.com/pyvista/pyvista/issues
|
||||
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrameServerDownError(RuntimeError): # numpydoc ignore=PR01
|
||||
"""Exception when trame server is down for Jupyter."""
|
||||
|
||||
def __init__(self, server_name):
|
||||
"""Call the base class constructor with the custom message."""
|
||||
super().__init__(SERVER_DOWN_MESSAGE.format(name=server_name))
|
||||
|
||||
|
||||
class TrameJupyterServerDownError(RuntimeError):
|
||||
"""Exception when trame server is down for Jupyter."""
|
||||
|
||||
def __init__(self):
|
||||
"""Call the base class constructor with the custom message."""
|
||||
# Be incredibly verbose on how users should launch trame server
|
||||
# Both warn so it appears at top
|
||||
warnings.warn(JUPYTER_SERVER_DOWN_MESSAGE)
|
||||
# and Error
|
||||
super().__init__(JUPYTER_SERVER_DOWN_MESSAGE)
|
||||
|
||||
|
||||
class Widget(HTML): # type: ignore[misc] # numpydoc ignore=PR01
|
||||
"""Custom HTML iframe widget for trame viewer."""
|
||||
|
||||
def __init__(self, viewer, src, width=None, height=None, iframe_attrs=None, **kwargs):
|
||||
"""Initialize."""
|
||||
if HTML is object:
|
||||
msg = 'Please install `ipywidgets`.'
|
||||
raise ImportError(msg)
|
||||
# eventually we could maybe expose this, but for now make sure we're at least
|
||||
# consistent with matplotlib's color (light gray)
|
||||
|
||||
if iframe_attrs is None:
|
||||
iframe_attrs = {}
|
||||
|
||||
border = 'border: 1px solid rgb(221,221,221);'
|
||||
|
||||
iframe_attrs = {
|
||||
**iframe_attrs,
|
||||
'src': src,
|
||||
'class': 'pyvista',
|
||||
'style': f'width: {width}; height: {height}; {border}',
|
||||
}
|
||||
|
||||
iframe_attrs_str = ' '.join(f'{key}="{value!s}"' for key, value in iframe_attrs.items())
|
||||
|
||||
value = f'<iframe {iframe_attrs_str}></iframe>'
|
||||
|
||||
super().__init__(value, **kwargs)
|
||||
self._viewer = viewer
|
||||
self._src = src
|
||||
|
||||
@property
|
||||
def viewer(self): # numpydoc ignore=RT01
|
||||
"""Get the associated viewer instance."""
|
||||
return self._viewer
|
||||
|
||||
@property
|
||||
def src(self): # numpydoc ignore=RT01
|
||||
"""Get the src URL."""
|
||||
return self._src
|
||||
|
||||
|
||||
class EmbeddableWidget(HTML): # type: ignore[misc] # numpydoc ignore=PR01
|
||||
"""Custom HTML iframe widget for embedding the trame viewer."""
|
||||
|
||||
def __init__(self, plotter, width, height, **kwargs):
|
||||
"""Initialize."""
|
||||
if HTML is object:
|
||||
msg = 'Please install `ipywidgets`.'
|
||||
raise ImportError(msg)
|
||||
scene = plotter.export_html(filename=None)
|
||||
src = scene.getvalue().replace('"', '"')
|
||||
# eventually we could maybe expose this, but for now make sure we're at least
|
||||
# consistent with matplotlib's color (light gray)
|
||||
border = 'border: 1px solid rgb(221,221,221);'
|
||||
value = (
|
||||
f'<iframe srcdoc="{src}" class="pyvista" style="width: {width}; '
|
||||
f'height: {height}; {border}"></iframe>'
|
||||
)
|
||||
super().__init__(value, **kwargs)
|
||||
self._src = src
|
||||
|
||||
|
||||
def launch_server(server=None, port=None, host=None, wslink_backend=None, **kwargs):
|
||||
"""Launch a trame server for use with Jupyter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
server : str, optional
|
||||
By default this uses :attr:`pyvista.global_theme.trame.jupyter_server_name
|
||||
<pyvista.plotting.themes._TrameConfig.jupyter_server_name>`, which by default is
|
||||
set to ``'pyvista-jupyter'``.
|
||||
|
||||
If a server name is given and such server is not available yet, it will
|
||||
be created otherwise the previously created instance will be returned.
|
||||
|
||||
port : int, optional
|
||||
The port on which to bind the server. Defaults to 0 to automatically
|
||||
find an available port.
|
||||
|
||||
host : str, optional
|
||||
The host name to bind the server to on launch. Server will bind to
|
||||
``127.0.0.1`` by default unless user sets the environment variable ``TRAME_DEFAULT_HOST``.
|
||||
|
||||
wslink_backend : str, optional
|
||||
The wslink backend that the server should use
|
||||
``aiohttp`` by default, ``jupyter`` if the
|
||||
`trame_jupyter_extension <https://github.com/Kitware/trame-jupyter-extension>`_
|
||||
is used.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Any additional keyword arguments to pass to ``pyvista.trame.views.get_server``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
trame_server.core.Server
|
||||
The launched Trame server. To ``await`` the launch, use the
|
||||
``.ready`` future attribute on the server.
|
||||
|
||||
"""
|
||||
if server is None:
|
||||
server = pyvista.global_theme.trame.jupyter_server_name
|
||||
if isinstance(server, str):
|
||||
server = get_server(server, **kwargs)
|
||||
if port is None:
|
||||
port = pyvista.global_theme.trame.jupyter_server_port
|
||||
if host is None:
|
||||
# Default to `127.0.0.1` unless user sets TRAME_DEFAULT_HOST
|
||||
host = os.environ.get('TRAME_DEFAULT_HOST', '127.0.0.1')
|
||||
if (
|
||||
wslink_backend is None and pyvista.global_theme.trame.jupyter_extension_enabled
|
||||
): # pragma: no cover
|
||||
wslink_backend = 'jupyter'
|
||||
|
||||
# Must enable all used modules
|
||||
html_widgets.initialize(server)
|
||||
vtk_widgets.initialize(server)
|
||||
|
||||
if server.client_type == 'vue2':
|
||||
vuetify2_widgets.initialize(server)
|
||||
else:
|
||||
vuetify3_widgets.initialize(server)
|
||||
|
||||
def on_ready(**_):
|
||||
logger.debug(f'Server ready: {server}')
|
||||
|
||||
if server._running_stage == 0:
|
||||
server.controller.on_server_ready.add(on_ready)
|
||||
server.start(
|
||||
exec_mode='task',
|
||||
host=host,
|
||||
port=port,
|
||||
open_browser=False,
|
||||
show_connection_info=False,
|
||||
disable_logging=True,
|
||||
timeout=0,
|
||||
backend=wslink_backend,
|
||||
)
|
||||
# else, server is already running or launching
|
||||
return server
|
||||
|
||||
|
||||
def build_url(
|
||||
_server,
|
||||
ui=None,
|
||||
server_proxy_enabled=None,
|
||||
server_proxy_prefix=None,
|
||||
host='localhost',
|
||||
protocol='http',
|
||||
): # numpydoc ignore=PR01,RT01
|
||||
"""Build the URL for the iframe."""
|
||||
params = f'?ui={ui}&reconnect=auto' if ui else '?reconnect=auto'
|
||||
if server_proxy_enabled is None:
|
||||
server_proxy_enabled = pyvista.global_theme.trame.server_proxy_enabled
|
||||
if server_proxy_enabled:
|
||||
if server_proxy_prefix is None:
|
||||
server_proxy_prefix = pyvista.global_theme.trame.server_proxy_prefix
|
||||
# server_proxy_prefix assumes trailing slash
|
||||
prefix = server_proxy_prefix if server_proxy_prefix else ''
|
||||
src = f'{prefix}{_server.port}/index.html{params}'
|
||||
else:
|
||||
src = f'{protocol}://{host}:{_server.port}/index.html{params}'
|
||||
logger.debug(src)
|
||||
return src
|
||||
|
||||
|
||||
def initialize(
|
||||
server,
|
||||
plotter,
|
||||
mode=None,
|
||||
default_server_rendering=True,
|
||||
collapse_menu=False,
|
||||
**kwargs,
|
||||
): # numpydoc ignore=PR01,RT01
|
||||
"""Generate the UI for a given plotter."""
|
||||
state = server.state
|
||||
state.trame__title = UI_TITLE
|
||||
|
||||
viewer = get_viewer(
|
||||
plotter,
|
||||
server=server,
|
||||
suppress_rendering=mode == 'client',
|
||||
)
|
||||
|
||||
with viewer.make_layout(server, template_name=plotter._id_name) as layout:
|
||||
viewer.layout = layout
|
||||
viewer.ui(
|
||||
mode=mode,
|
||||
default_server_rendering=default_server_rendering,
|
||||
collapse_menu=collapse_menu,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return viewer
|
||||
|
||||
|
||||
def show_trame(
|
||||
plotter: Plotter,
|
||||
mode: JupyterBackendOptions | None = None,
|
||||
name: str | None = None,
|
||||
server_proxy_enabled: bool | None = None,
|
||||
server_proxy_prefix: str | None = None,
|
||||
jupyter_extension_enabled: bool | None = None,
|
||||
collapse_menu: bool = False,
|
||||
add_menu: bool = True,
|
||||
add_menu_items: Callable[[Literal['trame', 'server', 'client'], bool, bool], None]
|
||||
| None = None,
|
||||
default_server_rendering: bool = True,
|
||||
handler: Callable[Concatenate[Viewer, str, ...], IFrame] | None = None,
|
||||
**kwargs,
|
||||
) -> EmbeddableWidget | IFrame | Widget:
|
||||
"""Run and display the trame application in jupyter's event loop.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plotter : pyvista.Plotter
|
||||
The PyVista plotter to show.
|
||||
|
||||
mode : str, optional
|
||||
The UI view mode. This can be set on the global theme. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
* ``'html'``: Exports the scene for client rendering that can be
|
||||
embedded in a webpage.
|
||||
|
||||
name : str, optional
|
||||
The name of the trame server on which the UI is defined.
|
||||
|
||||
server_proxy_enabled : bool, default: False
|
||||
Build a relative URL for use with ``jupyter-server-proxy``.
|
||||
|
||||
server_proxy_prefix : str, optional
|
||||
URL prefix when using ``server_proxy_enabled``. This can be set
|
||||
globally in the theme. To ignore, pass ``False``. For use with
|
||||
``jupyter-server-proxy``, often set to ``proxy/``.
|
||||
|
||||
jupyter_extension_enabled : bool, default: False
|
||||
Build a relative URL for use with ``trame-jupyter-extension``.
|
||||
|
||||
collapse_menu : bool, default: False
|
||||
Collapse the UI menu (camera controls, etc.) on start.
|
||||
|
||||
add_menu : bool, default: True
|
||||
Add a UI controls VCard to the VContainer.
|
||||
|
||||
add_menu_items : callable, default: None
|
||||
Append more UI controls to the VCard menu. Should be a function similar to
|
||||
Viewer.ui_controls().
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
handler : callable, optional
|
||||
Pass a callable that accptes the viewer instance, the string URL,
|
||||
and ``**kwargs`` to create custom HTML representations of the output.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pyvista as pv
|
||||
from IPython.display import IFrame
|
||||
|
||||
mesh = pyvista.Wavelet()
|
||||
|
||||
|
||||
def handler(viewer, src, **kwargs):
|
||||
return IFrame(src, '75%', '500px')
|
||||
|
||||
|
||||
p = pyvista.Plotter(notebook=True)
|
||||
_ = p.add_mesh(mesh)
|
||||
iframe = p.show(
|
||||
jupyter_backend='trame',
|
||||
jupyter_kwargs=dict(handler=handler),
|
||||
return_viewer=True,
|
||||
)
|
||||
iframe
|
||||
|
||||
**kwargs : dict, optional
|
||||
Mostly ignored, though ``protocol`` and ``host`` can be use to
|
||||
override the iframe src url and ``height`` and ``width`` can be
|
||||
used to override the iframe style. Remaining kwargs are passed to
|
||||
``ipywidgets.widgets.HTML``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ipywidgets.widgets.HTML or handler result
|
||||
Returns a HTML IFrame widget or the result of the passed handler.
|
||||
|
||||
"""
|
||||
if plotter.render_window is None:
|
||||
raise RuntimeError(CLOSED_PLOTTER_ERROR)
|
||||
|
||||
if plotter._window_size_unset:
|
||||
dw, dh = '99%', '600px'
|
||||
else:
|
||||
width, height = plotter.window_size
|
||||
dw = f'{width}px'
|
||||
dh = f'{height}px'
|
||||
kwargs.setdefault('width', dw)
|
||||
kwargs.setdefault('height', dh)
|
||||
|
||||
if mode == 'html':
|
||||
return EmbeddableWidget(plotter, **kwargs)
|
||||
|
||||
if jupyter_extension_enabled is None:
|
||||
jupyter_extension_enabled = pyvista.global_theme.trame.jupyter_extension_enabled
|
||||
|
||||
if name is None:
|
||||
server = get_server(name=pyvista.global_theme.trame.jupyter_server_name)
|
||||
else:
|
||||
server = get_server(name=name)
|
||||
if name is None and not server.running:
|
||||
wslink_backend = 'aiohttp'
|
||||
if jupyter_extension_enabled: # pragma: no cover
|
||||
wslink_backend = 'jupyter'
|
||||
|
||||
elegantly_launch(server, wslink_backend=wslink_backend)
|
||||
if not server.running: # pragma: no cover
|
||||
raise TrameJupyterServerDownError
|
||||
elif not server.running:
|
||||
raise TrameServerDownError(name)
|
||||
|
||||
# Initialize app
|
||||
viewer = initialize(
|
||||
server,
|
||||
plotter,
|
||||
mode=mode,
|
||||
default_server_rendering=default_server_rendering,
|
||||
collapse_menu=collapse_menu,
|
||||
add_menu=add_menu,
|
||||
add_menu_items=add_menu_items,
|
||||
)
|
||||
|
||||
if jupyter_extension_enabled: # pragma: no cover
|
||||
from trame_client.ui.core import iframe_url_builder_jupyter_extension # noqa: PLC0415
|
||||
|
||||
iframe_attrs = iframe_url_builder_jupyter_extension(viewer.layout)
|
||||
src = iframe_attrs['src']
|
||||
else:
|
||||
# TODO: The build_url function could possibly be replaced by
|
||||
# trame's upstream url builders in trame_client.ui.core
|
||||
iframe_attrs = {}
|
||||
src = build_url(
|
||||
server,
|
||||
ui=plotter._id_name,
|
||||
server_proxy_enabled=server_proxy_enabled,
|
||||
server_proxy_prefix=server_proxy_prefix,
|
||||
host=kwargs.get('host', 'localhost'),
|
||||
protocol=kwargs.get('protocol', 'http'),
|
||||
)
|
||||
|
||||
if callable(handler):
|
||||
return handler(viewer, src, iframe_attrs=iframe_attrs, **kwargs)
|
||||
return Widget(viewer, src, iframe_attrs=iframe_attrs, **kwargs)
|
||||
|
||||
|
||||
def elegantly_launch(*args, **kwargs): # numpydoc ignore=PR01
|
||||
"""Elegantly launch the Trame server without await.
|
||||
|
||||
This provides a mechanism to launch the Trame Jupyter backend in
|
||||
a way that does not require users to await the call.
|
||||
|
||||
This is a thin wrapper of
|
||||
:func:`launch_server() <pyvista.trame.jupyter.launch_server>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
trame_server.core.Server
|
||||
The launched trame server.
|
||||
|
||||
Warnings
|
||||
--------
|
||||
This uses `nest_asyncio <https://github.com/erdewit/nest_asyncio>`_ which
|
||||
patches the standard lib `asyncio` package and may have unintended
|
||||
consequences for some uses cases. We advise strongly to make sure PyVista's
|
||||
Jupyter backend is not set to use Trame when not in a Jupyter environment.
|
||||
|
||||
"""
|
||||
try:
|
||||
import nest_asyncio # noqa: PLC0415
|
||||
except ImportError:
|
||||
msg = (
|
||||
'Please install `nest_asyncio` to automagically launch the trame server '
|
||||
'without await. Or, to avoid `nest_asynctio` run:\n\n'
|
||||
'from pyvista.trame.jupyter import launch_server\n'
|
||||
'await launch_server().ready'
|
||||
)
|
||||
raise ImportError(msg)
|
||||
|
||||
async def launch_it():
|
||||
await launch_server(*args, **kwargs).ready
|
||||
|
||||
# Basically monkey patches asyncio to support this
|
||||
nest_asyncio.apply()
|
||||
|
||||
return asyncio.run(launch_it())
|
||||
@@ -0,0 +1,120 @@
|
||||
"""PyVista Trame User Interface.
|
||||
|
||||
This module builds a base UI for manipulating a PyVista Plotter.
|
||||
The UI generated here is the default for rendering in Jupyter
|
||||
environments and provides a starting point for custom user-built
|
||||
applications.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import warnings
|
||||
|
||||
from trame.app import get_server
|
||||
|
||||
from .vuetify2 import Viewer as Vue2Viewer
|
||||
from .vuetify3 import Viewer as Vue3Viewer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base_viewer import BaseViewer
|
||||
|
||||
_VIEWERS: dict[str, BaseViewer] = {}
|
||||
UI_TITLE = 'PyVista'
|
||||
|
||||
|
||||
def get_viewer(plotter, server=None, suppress_rendering=False):
|
||||
"""Get a Viewer instance for a given Plotter.
|
||||
|
||||
There should be only one Viewer instance per plotter. A Viewer
|
||||
can have multiple UI views though.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plotter : pyvista.Plotter
|
||||
Plotter to return or create the viewer instance for.
|
||||
|
||||
server : trame.Server, optional
|
||||
Current server for Trame application.
|
||||
|
||||
suppress_rendering : bool, default: False
|
||||
Suppress rendering on the plotter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pyvista.trame.ui.Viewer
|
||||
Trame viewer.
|
||||
|
||||
"""
|
||||
if plotter._id_name in _VIEWERS:
|
||||
viewer = _VIEWERS[plotter._id_name]
|
||||
if suppress_rendering != plotter.suppress_rendering:
|
||||
plotter.suppress_rendering = suppress_rendering
|
||||
warnings.warn(
|
||||
'Suppress rendering on the plotter is changed to ' + str(suppress_rendering),
|
||||
UserWarning,
|
||||
)
|
||||
return viewer
|
||||
|
||||
if not server:
|
||||
server = get_server()
|
||||
if server.client_type == 'vue2':
|
||||
viewer = Vue2Viewer(plotter, suppress_rendering=suppress_rendering, server=server)
|
||||
else:
|
||||
viewer = Vue3Viewer(plotter, suppress_rendering=suppress_rendering, server=server)
|
||||
|
||||
_VIEWERS[plotter._id_name] = viewer
|
||||
return viewer
|
||||
|
||||
|
||||
def plotter_ui(
|
||||
plotter,
|
||||
mode=None,
|
||||
default_server_rendering=True,
|
||||
collapse_menu=False,
|
||||
add_menu=True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Create a UI view for the given Plotter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plotter : pyvista.Plotter
|
||||
Plotter to create the UI for.
|
||||
|
||||
mode : str, default: 'trame'
|
||||
The UI view mode. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
collapse_menu : bool, default: False
|
||||
Collapse the UI menu (camera controls, etc.) on start.
|
||||
|
||||
add_menu : bool, default: True
|
||||
Add a UI controls VCard to the VContainer.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Additional keyword arguments are passed to the viewer being created.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PyVistaRemoteLocalView | PyVistaRemoteView | PyVistaLocalView
|
||||
Trame view interface for pyvista.
|
||||
|
||||
"""
|
||||
viewer = get_viewer(plotter, server=kwargs.get('server'), suppress_rendering=mode == 'client')
|
||||
return viewer.ui(
|
||||
mode=mode,
|
||||
default_server_rendering=default_server_rendering,
|
||||
collapse_menu=collapse_menu,
|
||||
add_menu=add_menu,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,306 @@
|
||||
"""PyVista Trame Base Viewer class.
|
||||
|
||||
This base class defines methods to manipulate a PyVista Plotter.
|
||||
This base class does not define a `ui` method, but its derived classes do.
|
||||
See `pyvista.trame.ui.vuetify2` and ``pyvista.trame.ui.vuetify3` for its derived classes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from trame.app import get_server
|
||||
|
||||
import pyvista
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trame_client.ui.core import AbstractLayout
|
||||
|
||||
|
||||
class BaseViewer:
|
||||
"""Internal wrapper to sync trame view with Plotter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plotter : pyvista.Plotter
|
||||
Target Plotter instance to view.
|
||||
server : trame.Server, optional
|
||||
Current Server for Trame Application.
|
||||
suppress_rendering : bool, default=False
|
||||
Whether to suppress rendering on the Plotter.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, plotter, server=None, suppress_rendering=False):
|
||||
"""Initialize Viewer."""
|
||||
self._html_views = set()
|
||||
|
||||
if server is None:
|
||||
server = get_server()
|
||||
self.server = server
|
||||
self.plotter = plotter
|
||||
self.plotter.suppress_rendering = suppress_rendering
|
||||
|
||||
# State variable names
|
||||
self.SHOW_UI = f'{plotter._id_name}_show_ui'
|
||||
self.GRID = f'{plotter._id_name}_grid_visibility'
|
||||
self.OUTLINE = f'{plotter._id_name}_outline_visibility'
|
||||
self.EDGES = f'{plotter._id_name}_edge_visibility'
|
||||
self.AXIS = f'{plotter._id_name}_axis_visibility'
|
||||
self.PARALLEL = f'{plotter._id_name}_parallel_projection'
|
||||
self.SERVER_RENDERING = f'{plotter._id_name}_use_server_rendering'
|
||||
self.VALID_UI_MODES = [
|
||||
'trame',
|
||||
'client',
|
||||
'server',
|
||||
]
|
||||
server.state[self.SHOW_UI] = True
|
||||
server.state[self.GRID] = False
|
||||
server.state[self.OUTLINE] = False
|
||||
server.state[self.EDGES] = False
|
||||
server.state[self.AXIS] = False
|
||||
server.state[self.PARALLEL] = False
|
||||
|
||||
@property
|
||||
def views(self): # numpydoc ignore=RT01
|
||||
"""Get a set of all associate trame views for this viewer."""
|
||||
return self._html_views
|
||||
|
||||
def update(self, **kwargs): # noqa: ARG002
|
||||
"""Update all associated views.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
for view in self._html_views:
|
||||
view.update()
|
||||
|
||||
def push_camera(self, **kwargs): # noqa: ARG002
|
||||
"""Push camera to all associated views.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
for view in self._html_views:
|
||||
view.push_camera()
|
||||
|
||||
def reset_camera(self, **kwargs): # noqa: ARG002
|
||||
"""Reset camera for all associated views.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
for view in self._html_views:
|
||||
view.reset_camera()
|
||||
|
||||
def update_image(self, **kwargs): # noqa: ARG002
|
||||
"""Update image for all associated views.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
for view in self._html_views:
|
||||
view.update_image()
|
||||
|
||||
def update_camera(self, **kwargs): # noqa: ARG002
|
||||
"""Update image and camera for all associated views.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
for view in self._html_views:
|
||||
view.update_camera()
|
||||
|
||||
def view_isometric(self):
|
||||
"""View isometric."""
|
||||
self.plotter.view_isometric(render=False)
|
||||
self.update_camera()
|
||||
|
||||
def view_yz(self):
|
||||
"""View YZ plane."""
|
||||
self.plotter.view_yz(render=False)
|
||||
self.update_camera()
|
||||
|
||||
def view_xz(self):
|
||||
"""View XZ plane."""
|
||||
self.plotter.view_xz(render=False)
|
||||
self.update_camera()
|
||||
|
||||
def view_xy(self):
|
||||
"""View XY plane."""
|
||||
self.plotter.view_xy(render=False)
|
||||
self.update_camera()
|
||||
|
||||
def on_parallel_projection_change(self, **kwargs):
|
||||
"""Toggle parallel projection for all renderers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
value = kwargs[self.PARALLEL]
|
||||
for renderer in self.plotter.renderers:
|
||||
if value:
|
||||
renderer.enable_parallel_projection()
|
||||
else:
|
||||
renderer.disable_parallel_projection()
|
||||
self.update()
|
||||
|
||||
def on_edge_visibility_change(self, **kwargs):
|
||||
"""Toggle edge visibility for all actors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
value = kwargs[self.EDGES]
|
||||
for renderer in self.plotter.renderers:
|
||||
for actor in renderer.actors.values():
|
||||
if isinstance(actor, pyvista.Actor):
|
||||
actor.prop.show_edges = value
|
||||
self.update()
|
||||
|
||||
def on_grid_visibility_change(self, **kwargs):
|
||||
"""Handle axes grid visibility.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
value = kwargs[self.GRID]
|
||||
for renderer in self.plotter.renderers:
|
||||
if value:
|
||||
renderer.show_grid()
|
||||
else:
|
||||
renderer.remove_bounds_axes()
|
||||
self.update()
|
||||
|
||||
def on_outline_visibility_change(self, **kwargs):
|
||||
"""Handle outline visibility.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
value = kwargs[self.OUTLINE]
|
||||
for renderer in self.plotter.renderers:
|
||||
if value:
|
||||
renderer.add_bounding_box(reset_camera=False)
|
||||
else:
|
||||
renderer.remove_bounding_box()
|
||||
self.update()
|
||||
|
||||
def on_axis_visibility_change(self, **kwargs):
|
||||
"""Handle outline visibility.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
value = kwargs[self.AXIS]
|
||||
for renderer in self.plotter.renderers:
|
||||
if value:
|
||||
renderer.show_axes()
|
||||
else:
|
||||
renderer.hide_axes()
|
||||
for view in self._html_views:
|
||||
if view.set_widgets:
|
||||
# VtkRemoteView does not have set_widgets function, but
|
||||
# VtkRemoteLocalView and VtkLocalView do.
|
||||
view.set_widgets(
|
||||
[
|
||||
ren.axes_widget
|
||||
for ren in self.plotter.renderers
|
||||
if ren.axes_widget is not None
|
||||
],
|
||||
)
|
||||
self.update()
|
||||
|
||||
def on_rendering_mode_change(self, **kwargs):
|
||||
"""Handle any configurations when the render mode changes between client and server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs : dict, optional
|
||||
Unused keyword arguments.
|
||||
|
||||
"""
|
||||
if not kwargs[self.SERVER_RENDERING]:
|
||||
self.update_camera()
|
||||
|
||||
@property
|
||||
def actors(self): # numpydoc ignore=RT01
|
||||
"""Get dataset actors."""
|
||||
return {k: v for k, v in self.plotter.actors.items() if isinstance(v, pyvista.Actor)}
|
||||
|
||||
def screenshot(self):
|
||||
"""Take screenshot and add attachament.
|
||||
|
||||
Returns
|
||||
-------
|
||||
memoryview
|
||||
Screenshot as a ``memoryview``.
|
||||
|
||||
"""
|
||||
self.plotter.render()
|
||||
self.update() # makes sure the plotter and views are in sync
|
||||
buffer = io.BytesIO()
|
||||
self.plotter.screenshot(filename=buffer)
|
||||
buffer.seek(0)
|
||||
return memoryview(buffer.read())
|
||||
|
||||
def export(self): # numpydoc ignore=RT01
|
||||
"""Export the scene as a zip file."""
|
||||
for view in self._html_views:
|
||||
return memoryview(view.export_html())
|
||||
msg = 'This viewer cannot be exported.'
|
||||
raise TypeError(msg)
|
||||
|
||||
def ui(self):
|
||||
"""Implement in derived classes."""
|
||||
raise NotImplementedError
|
||||
|
||||
def make_layout(self, *args, **kwargs) -> AbstractLayout: # pragma: no cover
|
||||
"""Create an instance of an AbstractLayout which is appropriate for a concrete viewer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args : tuple
|
||||
Positional arguments.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
AbstractLayout
|
||||
A layout this viewer can be embedded in.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,345 @@
|
||||
"""PyVista Trame Viewer class for a Vue 2 client.
|
||||
|
||||
This class, derived from `pyvista.trame.ui.base_viewer`,
|
||||
is intended for use with a trame application where the client type is "vue2".
|
||||
Therefore, the `ui` method implemented by this class utilizes the API of Vuetify 2.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from trame.ui.vuetify2 import VAppLayout
|
||||
from trame.widgets import html
|
||||
from trame.widgets import vuetify
|
||||
|
||||
from pyvista.trame.views import PyVistaLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteView
|
||||
|
||||
from .base_viewer import BaseViewer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trame_client.ui.core import AbstractLayout
|
||||
|
||||
|
||||
def button(click, icon, tooltip): # numpydoc ignore=PR01
|
||||
"""Create a vuetify button."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator='{ on, attrs }'):
|
||||
with vuetify.VBtn(icon=True, v_bind='attrs', v_on='on', click=click):
|
||||
vuetify.VIcon(icon)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def checkbox(model, icons, tooltip): # numpydoc ignore=PR01
|
||||
"""Create a vuetify checkbox."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator='{ on, attrs }'):
|
||||
with html.Div(v_on='on', v_bind='attrs'):
|
||||
vuetify.VCheckbox(
|
||||
v_model=model,
|
||||
on_icon=icons[0],
|
||||
off_icon=icons[1],
|
||||
dense=True,
|
||||
hide_details=True,
|
||||
classes='my-0 py-0 ml-1',
|
||||
)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def slider(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify slider."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator='{ on, attrs }'):
|
||||
with html.Div(v_on='on', v_bind='attrs'):
|
||||
vuetify.VSlider(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def text_field(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify text field."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator='{ on, attrs }'):
|
||||
with html.Div(v_on='on', v_bind='attrs'):
|
||||
vuetify.VTextField(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def select(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify select menu."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator='{ on, attrs }'):
|
||||
with html.Div(v_on='on', v_bind='attrs'):
|
||||
vuetify.VSelect(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def divider(**kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify divider."""
|
||||
vuetify.VDivider(**kwargs)
|
||||
|
||||
|
||||
class Viewer(BaseViewer):
|
||||
"""Viewer implementation compatible with Vue 2 Trame Applications."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_layout(self, *args, **kwargs) -> AbstractLayout:
|
||||
"""Create instance of an AbstractLayout which is appropriate for this viewer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args : tuple
|
||||
Positional arguments.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
VAppLayout (vue2)
|
||||
A layout this viewer can be embedded in.
|
||||
|
||||
"""
|
||||
return VAppLayout(*args, **kwargs)
|
||||
|
||||
def ui_controls(self, mode=None, default_server_rendering=True, v_show=None):
|
||||
"""Create a VRow for the UI controls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str, default: 'trame'
|
||||
The UI view mode. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
v_show : bool, optional
|
||||
Conditionally show the viewer controls.
|
||||
|
||||
"""
|
||||
with vuetify.VRow(
|
||||
v_show=v_show,
|
||||
classes='pa-0 ma-0 align-center',
|
||||
) as row:
|
||||
server = row.server
|
||||
# Listen to state changes
|
||||
server.state.change(self.EDGES)(self.on_edge_visibility_change)
|
||||
server.state.change(self.GRID)(self.on_grid_visibility_change)
|
||||
server.state.change(self.OUTLINE)(self.on_outline_visibility_change)
|
||||
server.state.change(self.AXIS)(self.on_axis_visibility_change)
|
||||
server.state.change(self.PARALLEL)(self.on_parallel_projection_change)
|
||||
server.state.change(self.SERVER_RENDERING)(self.on_rendering_mode_change)
|
||||
vuetify.VDivider(vertical=True, classes='mr-1')
|
||||
button(
|
||||
click=self.reset_camera,
|
||||
icon='mdi-arrow-expand-all',
|
||||
tooltip='Reset Camera',
|
||||
)
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
button(
|
||||
click=self.view_isometric,
|
||||
icon='mdi-axis-arrow',
|
||||
tooltip='Perspective view',
|
||||
)
|
||||
button(
|
||||
click=self.view_yz,
|
||||
icon='mdi-axis-x-arrow',
|
||||
tooltip='Reset Camera X',
|
||||
)
|
||||
button(
|
||||
click=self.view_xz,
|
||||
icon='mdi-axis-y-arrow',
|
||||
tooltip='Reset Camera Y',
|
||||
)
|
||||
button(
|
||||
click=self.view_xy,
|
||||
icon='mdi-axis-z-arrow',
|
||||
tooltip='Reset Camera Z',
|
||||
)
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
checkbox(
|
||||
model=(self.EDGES, False),
|
||||
icons=('mdi-grid', 'mdi-grid-off'),
|
||||
tooltip=f"Toggle edge visibility ({{{{ {self.EDGES} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.OUTLINE, False),
|
||||
icons=('mdi-cube', 'mdi-cube-off'),
|
||||
tooltip=f"Toggle bounding box ({{{{ {self.OUTLINE} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.GRID, False),
|
||||
icons=('mdi-ruler-square', 'mdi-ruler-square'),
|
||||
tooltip=f"Toggle ruler ({{{{ {self.GRID} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.AXIS, False),
|
||||
icons=('mdi-axis-arrow-info', 'mdi-axis-arrow-info'),
|
||||
tooltip=f"Toggle axis ({{{{ {self.AXIS} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
# Server rendering options
|
||||
if mode == 'trame':
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
checkbox(
|
||||
model=(self.SERVER_RENDERING, default_server_rendering),
|
||||
icons=('mdi-dns', 'mdi-open-in-app'),
|
||||
tooltip=f'Toggle rendering mode '
|
||||
f"({{{{ {self.SERVER_RENDERING} ? 'remote' : 'local' }}}})",
|
||||
)
|
||||
with vuetify.VRow(
|
||||
v_show=(self.SERVER_RENDERING, default_server_rendering),
|
||||
classes='pa-0 ma-0 align-center',
|
||||
):
|
||||
checkbox(
|
||||
model=(self.PARALLEL, False),
|
||||
icons=('mdi-camera-off', 'mdi-camera-switch'),
|
||||
tooltip=f'Toggle parallel projection '
|
||||
f"({{{{ {self.PARALLEL} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
|
||||
def attach_screenshot():
|
||||
return server.protocol.addAttachment(self.screenshot())
|
||||
|
||||
button(
|
||||
# Must use single-quote string for JS here
|
||||
click=f"utils.download('screenshot.png', "
|
||||
f"trigger('{server.trigger_name(attach_screenshot)}'), "
|
||||
f"'image/png')",
|
||||
icon='mdi-file-png-box',
|
||||
tooltip='Save screenshot',
|
||||
)
|
||||
|
||||
def attach_export():
|
||||
return server.protocol.addAttachment(self.export())
|
||||
|
||||
button(
|
||||
# Must use single-quote string for JS here
|
||||
click="utils.download('scene-export.html', "
|
||||
f"trigger('{server.trigger_name(attach_export)}'), "
|
||||
"'application/octet-stream')",
|
||||
icon='mdi-download',
|
||||
tooltip='Export scene as HTML',
|
||||
)
|
||||
|
||||
def ui(
|
||||
self,
|
||||
mode=None,
|
||||
default_server_rendering=True,
|
||||
collapse_menu=False,
|
||||
add_menu=True,
|
||||
add_menu_items=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Generate VContainer for PyVista Plotter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str, default: 'trame'
|
||||
The UI view mode. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
collapse_menu : bool, default: False
|
||||
Collapse the UI menu (camera controls, etc.) on start.
|
||||
|
||||
add_menu : bool, default: True
|
||||
Add a UI controls VCard to the VContainer.
|
||||
|
||||
add_menu_items : callable, default: None
|
||||
Append more UI controls to the VCard menu. Should be a function similar to
|
||||
`Viewer.ui_controls()`.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Additional keyword arguments are passed to the view being created.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
|
||||
Trame view interface for pyvista.
|
||||
|
||||
"""
|
||||
if mode is None:
|
||||
mode = self.plotter._theme.trame.default_mode
|
||||
if mode not in self.VALID_UI_MODES:
|
||||
msg = f'`{mode}` is not a valid mode choice. Use one of: {self.VALID_UI_MODES}'
|
||||
raise ValueError(msg)
|
||||
if mode != 'trame':
|
||||
default_server_rendering = mode == 'server'
|
||||
|
||||
with vuetify.VContainer(
|
||||
fluid=True,
|
||||
classes='pa-0 fill-height',
|
||||
style='position: relative',
|
||||
trame_server=self.server,
|
||||
) as container:
|
||||
server = container.server
|
||||
# Initialize state variables
|
||||
server.state[self.EDGES] = False
|
||||
server.state[self.GRID] = self.plotter.renderer.cube_axes_actor is not None
|
||||
server.state[self.OUTLINE] = self.plotter.renderer._box_object is not None
|
||||
server.state[self.AXIS] = (
|
||||
self.plotter.renderer.axes_widget is not None
|
||||
and self.plotter.renderer.axes_widget.GetEnabled()
|
||||
)
|
||||
server.state[self.SERVER_RENDERING] = default_server_rendering
|
||||
if add_menu:
|
||||
server.state[self.SHOW_UI] = not collapse_menu
|
||||
with vuetify.VCard(
|
||||
style='position: absolute; top: 20px; left: 20px; z-index: 1; height: 36px;',
|
||||
classes=(f"{{ 'rounded-circle': !{self.SHOW_UI} }}",),
|
||||
) as self.menu:
|
||||
with vuetify.VRow(classes='pa-0 ma-0'):
|
||||
button(
|
||||
click=f'{self.SHOW_UI}=!{self.SHOW_UI}',
|
||||
icon='mdi-dots-vertical',
|
||||
tooltip=f"{{{{ {self.SHOW_UI} ? 'Hide' : 'Show' }}}} menu",
|
||||
)
|
||||
self.ui_controls(
|
||||
mode=mode,
|
||||
default_server_rendering=default_server_rendering,
|
||||
v_show=(f'{self.SHOW_UI}',),
|
||||
)
|
||||
if callable(add_menu_items):
|
||||
with vuetify.VRow(
|
||||
v_show=(f'{self.SHOW_UI}',),
|
||||
classes='pa-0 ma-0 align-center',
|
||||
):
|
||||
add_menu_items()
|
||||
if mode == 'trame':
|
||||
view = PyVistaRemoteLocalView(
|
||||
self.plotter,
|
||||
mode=(
|
||||
# Must use single-quote string for JS here
|
||||
f"{self.SERVER_RENDERING} ? 'remote' : 'local'",
|
||||
'remote' if default_server_rendering else 'local',
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
elif mode == 'server':
|
||||
view = PyVistaRemoteView(self.plotter, **kwargs)
|
||||
elif mode == 'client':
|
||||
view = PyVistaLocalView(self.plotter, **kwargs)
|
||||
|
||||
self._html_views.add(view)
|
||||
if add_menu:
|
||||
view.menu = self.menu
|
||||
|
||||
return view
|
||||
@@ -0,0 +1,356 @@
|
||||
"""PyVista Trame Viewer class for a Vue 3 client.
|
||||
|
||||
This class, derived from `pyvista.trame.ui.base_viewer`,
|
||||
is intended for use with a trame application where the client type is "vue3".
|
||||
Therefore, the `ui` method implemented by this class utilizes the API of Vuetify 3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from trame.ui.vuetify3 import VAppLayout
|
||||
from trame.widgets import html
|
||||
from trame.widgets import vuetify3 as vuetify
|
||||
|
||||
from pyvista.trame.views import PyVistaLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteLocalView
|
||||
from pyvista.trame.views import PyVistaRemoteView
|
||||
|
||||
from .base_viewer import BaseViewer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trame_client.ui.core import AbstractLayout
|
||||
|
||||
|
||||
def button(click, icon, tooltip): # numpydoc ignore=PR01
|
||||
"""Create a vuetify button."""
|
||||
with vuetify.VTooltip(location='bottom'):
|
||||
with vuetify.Template(v_slot_activator=('{ props }',)):
|
||||
with vuetify.VBtn(
|
||||
icon=True,
|
||||
v_bind=('props',),
|
||||
variant='text',
|
||||
size='small',
|
||||
click=click,
|
||||
):
|
||||
vuetify.VIcon(icon)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def checkbox(model, icons, tooltip): # numpydoc ignore=PR01
|
||||
"""Create a vuetify checkbox."""
|
||||
with vuetify.VTooltip(location='bottom'):
|
||||
with vuetify.Template(v_slot_activator=('{ props }',)):
|
||||
with html.Div(v_bind=('props',)):
|
||||
vuetify.VCheckbox(
|
||||
v_model=model,
|
||||
true_icon=icons[0],
|
||||
false_icon=icons[1],
|
||||
density='compact',
|
||||
hide_details=True,
|
||||
classes='ma-1 py-1',
|
||||
)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def slider(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify slider."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator=('{ props }',)):
|
||||
with html.Div(v_bind=('props',)):
|
||||
vuetify.VSlider(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def text_field(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify text field."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator=('{ props }',)):
|
||||
with html.Div(v_bind=('props',)):
|
||||
vuetify.VTextField(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def select(model, tooltip, **kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify select menu."""
|
||||
with vuetify.VTooltip(bottom=True):
|
||||
with vuetify.Template(v_slot_activator=('{ props }',)):
|
||||
with html.Div(v_bind=('props',)):
|
||||
vuetify.VSelect(v_model=model, **kwargs)
|
||||
html.Span(tooltip)
|
||||
|
||||
|
||||
def divider(**kwargs): # numpydoc ignore=PR01
|
||||
"""Create a vuetify divider."""
|
||||
vuetify.VDivider(**kwargs)
|
||||
|
||||
|
||||
class Viewer(BaseViewer):
|
||||
"""Viewer implementation compatible with Vue 3 Trame Applications."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_layout(self, *args, **kwargs) -> AbstractLayout:
|
||||
"""Create instance of an AbstractLayout which is appropriate for this viewer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args : tuple
|
||||
Positional arguments.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
VAppLayout (vue3)
|
||||
A layout this viewer can be embedded in.
|
||||
|
||||
"""
|
||||
return VAppLayout(*args, **kwargs)
|
||||
|
||||
def ui_controls(self, mode=None, default_server_rendering=True, v_show=None):
|
||||
"""Create a VRow for the UI controls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str, default: 'trame'
|
||||
The UI view mode. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
v_show : bool, optional
|
||||
Conditionally show the viewer controls.
|
||||
|
||||
"""
|
||||
with vuetify.VRow(
|
||||
v_show=v_show,
|
||||
classes='pa-0 ma-0 align-center fill-height',
|
||||
style='flex-wrap: nowrap',
|
||||
) as row:
|
||||
server = row.server
|
||||
# Listen to state changes
|
||||
server.state.change(self.EDGES)(self.on_edge_visibility_change)
|
||||
server.state.change(self.GRID)(self.on_grid_visibility_change)
|
||||
server.state.change(self.OUTLINE)(self.on_outline_visibility_change)
|
||||
server.state.change(self.AXIS)(self.on_axis_visibility_change)
|
||||
server.state.change(self.SERVER_RENDERING)(self.on_rendering_mode_change)
|
||||
server.state.change(self.PARALLEL)(self.on_parallel_projection_change)
|
||||
vuetify.VDivider(vertical=True, classes='mr-1')
|
||||
button(
|
||||
click=self.reset_camera,
|
||||
icon='mdi-arrow-expand-all',
|
||||
tooltip='Reset Camera',
|
||||
)
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
button(
|
||||
click=self.view_isometric,
|
||||
icon='mdi-axis-arrow',
|
||||
tooltip='Perspective view',
|
||||
)
|
||||
button(
|
||||
click=self.view_yz,
|
||||
icon='mdi-axis-x-arrow',
|
||||
tooltip='Reset Camera X',
|
||||
)
|
||||
button(
|
||||
click=self.view_xz,
|
||||
icon='mdi-axis-y-arrow',
|
||||
tooltip='Reset Camera Y',
|
||||
)
|
||||
button(
|
||||
click=self.view_xy,
|
||||
icon='mdi-axis-z-arrow',
|
||||
tooltip='Reset Camera Z',
|
||||
)
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
checkbox(
|
||||
model=(self.EDGES, False),
|
||||
icons=('mdi-grid', 'mdi-grid-off'),
|
||||
tooltip=f"Toggle edge visibility ({{{{ {self.EDGES} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.OUTLINE, False),
|
||||
icons=('mdi-cube', 'mdi-cube-off'),
|
||||
tooltip=f"Toggle bounding box ({{{{ {self.OUTLINE} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.GRID, False),
|
||||
icons=('mdi-ruler-square', 'mdi-ruler-square'),
|
||||
tooltip=f"Toggle ruler ({{{{ {self.GRID} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
checkbox(
|
||||
model=(self.AXIS, False),
|
||||
icons=('mdi-axis-arrow-info', 'mdi-axis-arrow-info'),
|
||||
tooltip=f"Toggle axis ({{{{ {self.AXIS} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
# Server rendering options
|
||||
if mode == 'trame':
|
||||
vuetify.VDivider(vertical=True, classes='mx-1')
|
||||
checkbox(
|
||||
model=(self.SERVER_RENDERING, default_server_rendering),
|
||||
icons=('mdi-dns', 'mdi-open-in-app'),
|
||||
tooltip=f'Toggle rendering mode '
|
||||
f"({{{{ {self.SERVER_RENDERING} ? 'remote' : 'local' }}}})",
|
||||
)
|
||||
with vuetify.VRow(
|
||||
v_show=(self.SERVER_RENDERING, default_server_rendering),
|
||||
classes='pa-0 ma-0 align-center fill-height',
|
||||
style='flex-wrap: nowrap; flex: unset',
|
||||
):
|
||||
checkbox(
|
||||
model=(self.PARALLEL, False),
|
||||
icons=('mdi-camera-off', 'mdi-camera-switch'),
|
||||
tooltip=f'Toggle parallel projection '
|
||||
f"({{{{ {self.PARALLEL} ? 'on' : 'off' }}}})",
|
||||
)
|
||||
|
||||
def attach_screenshot():
|
||||
return server.protocol.addAttachment(self.screenshot())
|
||||
|
||||
button(
|
||||
# Must use single-quote string for JS here
|
||||
click="utils.download('screenshot.png', "
|
||||
f"trigger('{server.trigger_name(attach_screenshot)}'), "
|
||||
"'image/png')",
|
||||
icon='mdi-file-png-box',
|
||||
tooltip='Save screenshot',
|
||||
)
|
||||
|
||||
def attach_export():
|
||||
return server.protocol.addAttachment(self.export())
|
||||
|
||||
button(
|
||||
# Must use single-quote string for JS here
|
||||
click="utils.download('scene-export.html', "
|
||||
f"trigger('{server.trigger_name(attach_export)}'), "
|
||||
"'application/octet-stream')",
|
||||
icon='mdi-download',
|
||||
tooltip='Export scene as HTML',
|
||||
)
|
||||
|
||||
def ui(
|
||||
self,
|
||||
mode=None,
|
||||
default_server_rendering=True,
|
||||
collapse_menu=False,
|
||||
add_menu=True,
|
||||
add_menu_items=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Generate VContainer for PyVista Plotter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str, default: 'trame'
|
||||
The UI view mode. Options are:
|
||||
|
||||
* ``'trame'``: Uses a view that can switch between client and server
|
||||
rendering modes.
|
||||
* ``'server'``: Uses a view that is purely server rendering.
|
||||
* ``'client'``: Uses a view that is purely client rendering (generally
|
||||
safe without a virtual frame buffer)
|
||||
|
||||
default_server_rendering : bool, default: True
|
||||
Whether to use server-side or client-side rendering on-start when
|
||||
using the ``'trame'`` mode.
|
||||
|
||||
collapse_menu : bool, default: False
|
||||
Collapse the UI menu (camera controls, etc.) on start.
|
||||
|
||||
add_menu : bool, default: True
|
||||
Add a UI controls VCard to the VContainer.
|
||||
|
||||
add_menu_items : callable, default: None
|
||||
Append more UI controls to the VCard menu. Should be a function similar to
|
||||
`Viewer.ui_controls()`.
|
||||
|
||||
**kwargs : dict, optional
|
||||
Additional keyword arguments are passed to the view being created.
|
||||
|
||||
Returns
|
||||
-------
|
||||
PyVistaRemoteLocalView, PyVistaRemoteView, or PyVistaLocalView
|
||||
Trame view interface for pyvista.
|
||||
|
||||
"""
|
||||
if mode is None:
|
||||
mode = self.plotter._theme.trame.default_mode
|
||||
if mode not in self.VALID_UI_MODES:
|
||||
msg = f'`{mode}` is not a valid mode choice. Use one of: {self.VALID_UI_MODES}'
|
||||
raise ValueError(msg)
|
||||
if mode != 'trame':
|
||||
default_server_rendering = mode == 'server'
|
||||
|
||||
with vuetify.VContainer(
|
||||
fluid=True,
|
||||
classes='pa-0 fill-height',
|
||||
style='position: relative',
|
||||
trame_server=self.server,
|
||||
) as container:
|
||||
server = container.server
|
||||
# Initialize state variables
|
||||
server.state[self.EDGES] = False
|
||||
server.state[self.GRID] = self.plotter.renderer.cube_axes_actor is not None
|
||||
server.state[self.OUTLINE] = self.plotter.renderer._box_object is not None
|
||||
server.state[self.AXIS] = (
|
||||
self.plotter.renderer.axes_widget is not None
|
||||
and self.plotter.renderer.axes_widget.GetEnabled()
|
||||
)
|
||||
server.state[self.SERVER_RENDERING] = default_server_rendering
|
||||
if add_menu:
|
||||
server.state[self.SHOW_UI] = not collapse_menu
|
||||
with vuetify.VCard(
|
||||
style='position: absolute; top: 20px; left: 20px; z-index: 1; height: 36px;',
|
||||
classes=(f"{{ 'rounded-circle': !{self.SHOW_UI} }}",),
|
||||
) as self.menu:
|
||||
with vuetify.VRow(
|
||||
classes='pa-0 ma-0 align-center fill-height',
|
||||
style='flex-wrap: nowrap',
|
||||
):
|
||||
button(
|
||||
click=f'{self.SHOW_UI}=!{self.SHOW_UI}',
|
||||
icon='mdi-dots-vertical',
|
||||
tooltip=f"{{{{ {self.SHOW_UI} ? 'Hide' : 'Show' }}}} menu",
|
||||
)
|
||||
self.ui_controls(
|
||||
mode=mode,
|
||||
default_server_rendering=default_server_rendering,
|
||||
v_show=(f'{self.SHOW_UI}',),
|
||||
)
|
||||
if callable(add_menu_items):
|
||||
with vuetify.VRow(
|
||||
v_show=(f'{self.SHOW_UI}',),
|
||||
classes='pa-0 ma-0 align-center',
|
||||
):
|
||||
add_menu_items()
|
||||
if mode == 'trame':
|
||||
view = PyVistaRemoteLocalView(
|
||||
self.plotter,
|
||||
mode=(
|
||||
# Must use single-quote string for JS here
|
||||
f"{self.SERVER_RENDERING} ? 'remote' : 'local'",
|
||||
'remote' if default_server_rendering else 'local',
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
elif mode == 'server':
|
||||
view = PyVistaRemoteView(self.plotter, **kwargs)
|
||||
elif mode == 'client':
|
||||
view = PyVistaLocalView(self.plotter, **kwargs)
|
||||
|
||||
self._html_views.add(view)
|
||||
if add_menu:
|
||||
view.menu = self.menu
|
||||
|
||||
return view
|
||||
@@ -0,0 +1,282 @@
|
||||
"""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]
|
||||
)
|
||||
Reference in New Issue
Block a user