Kitware / trame

Trame lets you weave various components and technologies into a Web Application solely written in Python.
https://kitware.github.io/trame/
Other
426 stars 56 forks source link

ctrl.view_reset_camera seems not working #422

Closed Jimmy-KL closed 9 months ago

Jimmy-KL commented 9 months ago

Describe the bug

I initialize the trame with no actors. Then I add an actor with a corresponding mesh. Also, I reset the camera and update the view. But nothing shows up. Then I add another button to reset the camera manually, then the mesh shows up. Alternatively, without manually resetting the camera, if I refresh the page, the mesh will show up too.

To Reproduce

Steps to reproduce the behavior:

  1. Click the "LOAD DATA" button, we can see the scalars bar, but there is no mesh. But actually, the mesh has been loaded.
  2. Refresh the page or click the Reset Camera button - at the top left, we will see the mesh.

Code Here is a simple example code to reproduce the bug.

from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import vuetify3

import pyvista as pv
from pyvista.trame import PyVistaLocalView

# -----------------------------------------------------------------------------
# Trame initialization
# -----------------------------------------------------------------------------
pv.global_theme.allow_empty_mesh = True
pv.OFF_SCREEN = True

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

# -----------------------------------------------------------------------------
# Callbacks
# -----------------------------------------------------------------------------

pl = pv.Plotter()

def read_mesh():
    mesh = pv.Sphere(center=(1000, 1000, 0))
    mesh["data"] = mesh.points[:, 0]
    actor = pl.add_mesh(mesh)
    ctrl.view_reset_camera()
    ctrl.view_update()

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

with SinglePageLayout(server) as layout:

    with layout.toolbar:
        vuetify3.VSpacer()
        vuetify3.VDivider(vertical=True, classes="mx-2")
        with vuetify3.VBtn("load data", click=read_mesh, style="margin-right: 10px;", variant="outlined"):
            vuetify3.VIcon("mdi-database-sync")
        with vuetify3.VBtn(icon=True, click=ctrl.view_reset_camera):
            vuetify3.VIcon("mdi-crop-free")   

    with layout.content:
        with vuetify3.VContainer(
            fluid=True,
            classes="pa-0 fill-height",
            style="position: relative;"
        ):
            view = PyVistaLocalView(pl)
            ctrl.view_update = view.update
            ctrl.view_reset_camera = view.reset_camera

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

if __name__ == "__main__":
    server.start()

Expected behavior

As I already wrote ctrl.view_reset_camera() and ctrl.view_update() in the functionread_mesh(), I think there is no need to click the Rest Camera button again manually. The mesh has been loaded, but it seems there is some problem with the camera which looks strange to me.

Screenshots 1) Click the "LOAD DATA" button image 2) Click the Reset Camera button image

Platform:

Device:

OS:

Browsers Affected:

jourdain commented 9 months ago

The issue is related to the fact that the reset_camera is evaluated locally (PyVistaLocalView) and at the time of the execution, the mesh is not yet available on the client side. Since you have the right camera information already available on the server side, it will be easier to call push_camera instead.

Jimmy-KL commented 9 months ago

I changed the code to:

def read_mesh():
    mesh = pv.Sphere(center=(1000, 1000, 0))
    mesh["data"] = mesh.points[:, 0]
    actor = pl.add_mesh(mesh)
    # ctrl.view_reset_camera()
    ctrl.view_push_camera()
    ctrl.view_update()

But it's still not working, the mesh didn't show up untile I click the reset camera button.

jourdain commented 9 months ago

You need to call reset camera on the server side to compute the camera position. Then, once you have the proper camera position on the server side, you can push that camera information to the client. Moreover, you probably need to call ctrl.view_update() first.

peterjwilson commented 7 months ago

I've used this approach with vtkLocalView to import multiple meshes (and the zoom updates to fit all bodies which is great), however the camera orientation resets to XY view after each import. Is there a way to maintain the camera orientation throughout the import?

jourdain commented 7 months ago

I'm not sure I understand your question or what you did. Calling reset camera should maintain whatever orientation you have. The only thing is that the local camera and the remote one are disconnected. So, if you want one to drive the other, you will need to sync them occasionally to ensure orientation consistency.

peterjwilson commented 7 months ago

Apologies if my question is unclear. The relevant parts of the importSTL function are:

def importSTL(importing_stl_file=None, **kwargs):
    ....
        renderer.AddActor(actor_stl)

        renderer.ResetCamera()
        ctrl.view_update()
        ctrl.view_reset_camera()
        ctrl.view_push_camera() #https://github.com/Kitware/trame/issues/422

Let's say I've imported a cylinder and have rotated it around: image

As soon as I import another mesh, the view orientation resets: image

jourdain commented 7 months ago

Thanks for providing that snippet of code, as it indeed explains what you see.

The main issue is that you push the server side camera that never saw your client side rotation.

Also calling ctrl.view_reset_camera() just after the view update won't work due to some asynchronous behavior. Basically at the time the "reset camera" is performed on the client side, the full new geometry is not yet available. So the computed bounds and so on is not what you expect.

The maybe a better approach for your usecase, could be to push (client to server) the camera at "end of interaction", so when you call renderer.ResetCamera() + ctrl.view_push_camera() you get the proper camera angle.

You can take cue on what we do with the Remote/Local view here

jourdain commented 7 months ago

Actually, what I listed above is in the wrong direction (remote -> local). What you want is local -> remote.

What you want is doing what is listed as an example here but by using the camera as arg (EndAnimation=(self.update_cam, "[$event]")) ( not sure the $event is the camera, but the JS expression can be tweaked to get it...)

peterjwilson commented 7 months ago

Absolutely fantastic @jourdain! Works perfectly! For future reference, code changes were:

def update_cam(event,**kwargs):
    poked_camera = event['pokedRenderer']['activeCamera']
    state.camera_orientation['position'] = poked_camera['position']
    state.camera_orientation['focal point'] = poked_camera['focalPoint']
    state.camera_orientation['view up'] = poked_camera['viewUp']
    state.camera_orientation['distance'] = poked_camera['distance']
    state.camera_orientation['clipping range'] = poked_camera['clippingRange']
    set_camera_orientation(renderer.GetActiveCamera(),state.camera_orientation)
def set_camera_orientation(camera, p):
    camera.SetPosition(p['position'])
    camera.SetFocalPoint(p['focal point'])
    camera.SetViewUp(p['view up'])
    camera.SetDistance(p['distance'])
    camera.SetClippingRange(p['clipping range'])
view = vtk.VtkLocalView(
                renderWindow,
                widgets=[orientation_axes],
                interactor_settings=("interactorSettings", VIEW_INTERACT),
                picking_modes=("[pickingMode]",),
                EndAnimation=(update_cam, "[$event]"), 
                click="pick_data = $event"
            )

Example of initial rotated mesh: image

Import of new mesh maintains camera orientation but resizes to fit all actors: image

jourdain commented 7 months ago

Side note, you can do EndAnimation=(update_cam, "[$event.pokedRenderer.activeCamera"), to only send the camera.