Kitware / trame-vtk

VTK/ParaView widgets for trame
BSD 3-Clause "New" or "Revised" License
17 stars 8 forks source link

Dynamically change the `interactive_ratio` on `VtkRemoteView` python-side #73

Open banesullivan-kobold opened 1 month ago

banesullivan-kobold commented 1 month ago

Is it possible to directly change the interactive_ratio from the VtkRemoteView instance itself in Python code? Take the following example with PyVista where I get access to the underlying VtkRemoteView instance and attempt to change the interactive_ratio. However, this change does not take affect (the dirty() call was my attempt at pushing this change):

import pyvista as pv

pl = pv.Plotter()
pl.add_mesh(pv.Wavelet())
w = pl.show(return_viewer=True)

for view in w._viewer.views:
    view.interactive_ratio = 4
    view.state.dirty('interactiveRatio')
    view.update()

w  # <-- repr in Jupyter noteook

I'm able to link the interactive_ratio to a variable, so I know it can be dynamically changed, but how can I do this directly from the VtkRemoteView instance python-side?

from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import vuetify3 as vuetify
from trame.widgets.vtk import VtkRemoteView

import pyvista as pv

server = get_server()
state, ctrl = server.state, server.controller

state.trame__title = "PyVista Remote View Ratios"

# -----------------------------------------------------------------------------

mesh = pv.Wavelet()

plotter = pv.Plotter(off_screen=True)
actor = plotter.add_mesh(mesh)
plotter.set_background("lightgrey")
plotter.view_isometric()

# -----------------------------------------------------------------------------
# GUI
# -----------------------------------------------------------------------------

with SinglePageLayout(server) as layout:
    layout.icon.click = ctrl.view_reset_camera
    layout.title.set_text(state.trame__title)

    with layout.toolbar:
        vuetify.VSpacer()
        vuetify.VSlider(
            label='Interactive ratio',
            v_model=('interactive_ratio', 2),
            min=0.05,
            max=4,
            step=0.05,
            hide_details=True,
            density='compact',
            style='max-width: 300px',
            # change=ctrl.view_update,
        )

    with layout.content:
        with vuetify.VContainer(
            fluid=True,
            classes="pa-0 fill-height",
        ):
            view = VtkRemoteView(plotter.ren_win, interactive_ratio=('interactive_ratio', 2), still_ratio=2)
            ctrl.view_update = view.update
            ctrl.view_reset_camera = view.reset_camera

# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------

if __name__ == "__main__":
    server.start()
jourdain commented 1 month ago

You keep having the same wrong assumption again and again. If you want to dynamically change something it needs to be in the state.

The attributes on the class is to just to define the HTML content which get evaluated only once. If you reflush the layout, you will see the change but that's really not what you want to do (lot of overhead of delete/create vue component client side).

So in other word, you need to generate the interactive ratio variable name and update it at the state level. That can be done at your wrapper class directly.

jourdain commented 1 month ago

What you have in mind is jupyter's trait, but that is really not what trame is doing.

banesullivan-kobold commented 1 month ago

You keep having the same wrong assumption again and again. If you want to dynamically change something it needs to be in the state. ... What you have in mind is jupyter's trait, but that is really not what trame is doing.

I find jupyter's traits rather intuitive. I can't help but wonder if trame could/should do this and synchronize the different attrs that may or may not be linked to state variables.

From my perspective, I don't see any reason why setting the interactive_ratio attr on the VtkRemoteView instance itself shouldn't propagate that change into the state.

I'm thinking something like the following:

class MyView(VtkRemoteView):

    def __init__(self, plotter, ref=None, **kwargs):
        self._INTERACTIVE_RATIO = f'{plotter._id_name}_interactive_ratio'
        if 'interactive_ratio' not in kwargs:
            kwargs['interactive_ratio'] = (self._INTERACTIVE_RATIO, 1)
        else:
            value = kwargs['interactive_ratio']
            if isinstance(value, tuple):
                self._INTERACTIVE_RATIO = value[0]
            else:
                kwargs['interactive_ratio'] = (self._INTERACTIVE_RATIO, value)
        super().__init__(plotter.render_window, ref, **kwargs)

    @property
    def interactive_ratio(self):
        return state[self._INTERACTIVE_RATIO]

    @interactive_ratio.setter
    def interactive_ratio(self, value):
        state[self._INTERACTIVE_RATIO] = value
        state.dirty(self._INTERACTIVE_RATIO)

Would it make sense for all attrs of of these types of classes be wrapped this way?

banesullivan-kobold commented 1 month ago

The attributes on the class is to just to define the HTML content which get evaluated only once. If you reflush the layout, you will see the change but that's really not what you want to do (lot of overhead of delete/create vue component client side).

So in other word, you need to generate the interactive ratio variable name and update it at the state level. That can be done at your wrapper class directly.

This is really helpful, thanks! I think this is something that should be implemented in PyVista's wrappings of these view classes and I'll try to make that proposal soon.

jourdain commented 1 month ago

So while I understand the jupyter/ipywidget approach make sense for tiny ui or set of controls, it does not when you create a full fledge client/server application where you have some performance consideration to keep in mind.

So, in short I'm fine if you want to add that binding logic on your component, but I'm not ok putting it by default in trame. (BTW your code won't work because of some logic in the AbstractWidgets which we may want to fix if we can)

What we could technically do is to trigger a flush on the layout when someone modify an attribute that is not a variable... There will be some flashing on the client side, but it will have the behavior you expect. Then we can teach users to better support dynamic behaviors.

banesullivan-kobold commented 1 month ago

For posterity, this can be done in PyVista via the following (I plan on pushing this into PyVista directly). @jourdain, is modifying the state via self.server.state like this okay?

from pyvista.trame.views import PyVistaRemoteView

class PyVistaRemoteInteractiveRatioView(PyVistaRemoteView):
    def __init__(self, plotter, **kwargs):
        self._INTERACTIVE_RATIO = f'{plotter._id_name}_interactive_ratio'
        self._STILL_RATIO = f'{plotter._id_name}_still_ratio'
        if 'interactive_ratio' not in kwargs:
            kwargs['interactive_ratio'] = (self._INTERACTIVE_RATIO, 1)
        else:
            value = kwargs['interactive_ratio']
            if isinstance(value, tuple):
                self._INTERACTIVE_RATIO = value[0]
            else:
                kwargs['interactive_ratio'] = (self._INTERACTIVE_RATIO, value)
        if 'still_ratio' not in kwargs:
            kwargs['still_ratio'] = (self._STILL_RATIO, 1)
        else:
            value = kwargs['still_ratio']
            if isinstance(value, tuple):
                self._STILL_RATIO = value[0]
            else:
                kwargs['still_ratio'] = (self._STILL_RATIO, value)
        super().__init__(plotter, **kwargs)

    def set_interactive_ratio(self, value):
        self.server.state[self._INTERACTIVE_RATIO] = value
        self.server.state.flush()

    def set_still_ratio(self, value):
        self.server.state[self._STILL_RATIO] = value
        self.server.state.flush()
banesullivan-kobold commented 1 month ago

In hindsight, I just noticed this is the same approach taken for toggling the remote/local modes on VtkRemoteLocalView. @jourdain, would it make sense to do this with all attributes under @property methods?

https://github.com/Kitware/trame-vtk/blob/a8ec763eb3a4a933939a77fd6a36751c2ef6cc94/trame_vtk/widgets/vtk/common.py#L554-L558

jourdain commented 1 month ago

Anything that you plan to use dynamically in PyVista, you can do it like you described.

But I would write the update like below

 def set_interactive_ratio(self, value):
        with self.state:
            self.state[self._INTERACTIVE_RATIO] = value