Kitware/trame

ctrl.view_reset_camera seems not working

Jimmy-KL opened this issue ยท 10 comments

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 function read_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:

  • Desktop
  • Mobile

OS:

  • Windows
  • MacOS
  • Linux
  • Android
  • iOS

Browsers Affected:

  • Chrome
  • Firefox
  • Microsoft Edge
  • Safari
  • Opera
  • Brave
  • IE 11

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.

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.

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.

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?

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.

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

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

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...)

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

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