init
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user