Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically change the interactive_ratio on VtkRemoteView python-side #73

Open
banesullivan-kobold opened this issue Sep 19, 2024 · 8 comments

Comments

@banesullivan-kobold
Copy link

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
Copy link
Collaborator

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
Copy link
Collaborator

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

@banesullivan-kobold
Copy link
Author

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
Copy link
Author

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
Copy link
Collaborator

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
Copy link
Author

banesullivan-kobold commented Oct 2, 2024

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
Copy link
Author

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?

def set_local_rendering(self, local=True, **kwargs):
self.server.state[self.__mode_key] = "local" if local else "remote"
def set_remote_rendering(self, remote=True, **kwargs):
self.server.state[self.__mode_key] = "remote" if remote else "local"

@jourdain
Copy link
Collaborator

jourdain commented Oct 3, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants