This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -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