Mayavi window embedded in WX editor fails to resize correctly following pyface invoke_later call to SetSize

I found a very strange bug in the 3D visualization library mayavi which I did not initially understand. I have tracked it down to a problem with wxpython.

In my use case, I create a Mayavi window inside a wx GUI (which happens to be created by traitsui but my example as follows bypasses the traitsui component of the problem). Then, using pyface.GUI.invoke_later() to immediately invoke some GUI events following the construction of the GUI and invocation of the event loop, I save some screen captures from mayavi, which under normal conditions resizes the window. However, in this use case, the window did not resize properly, and I have tracked the bug down to the call to SetSize( ) on the wxVTKRenderWindowInteractor which extends wx.GLCanvas.

Here is a minimal snippet which reliably reproduces the problem:

from traits.api import *
from traitsui.api import *
from mayavi import mlab
from mayavi.core.ui.api import MayaviScene, SceneEditor, MlabSceneModel
from pyface.api import GUI
import numpy as np

class MayaviGUIApp(HasTraits):
scene = Instance(MlabSceneModel,())
shell = Dict

@on_trait_change('scene.activated')
def setup(self):
    x,y,z = np.random.random((3,10))
    mlab.points3d(x,y,z,figure=self.scene.mayavi_scene)

traits_view = View(
    Item(name='scene',
         editor=SceneEditor(scene_class=MayaviScene),
         height=500, width=500, show_label=False, resizable=True),
    Item(name='shell',
         editor=ShellEditor(),
         height=300, width=500, show_label=False, resizable=True))

def invoke_script(scene=None):
# pass some script to this function and then execute it

#_vtk_control is a wxVTKRenderWindowInteractor which is a subclass of wx.GLCanvas
scene.scene.scene_editor._vtk_control.SetSize([375,375])
mlab.savefig('test_invokelater_1.png', figure=scene, magnification=2)
scene.scene.scene_editor._vtk_control.SetSize([500,468]) #set back to the original size

#this is equivalent to mlab.savefig('test_invokelater_1.png', size=(750,750), figure=scene), except the window
#resizing that is done in several levels of library calls eventually culminating in a wx canvas is managed manually.


#now if we try it again, it works this time.

scene.scene.scene_editor._vtk_control.SetSize([375,375])
mlab.savefig('test_invokelater_2.png', figure=scene, magnification=2)
scene.scene.scene_editor._vtk_control.SetSize([500,468]) #set back to the original size

mga = MayaviGUIApp()

gui = GUI() #from pyface import GUI
gui.invoke_later(lambda:invoke_script(scene=mga.scene.mayavi_scene))

invoke_later is just a toolkit-independent wrapper for wx.CallAfter

mga.configure_traits()

configure_traits sets up the GUI and then starts the event loop

This snippet reliably produces two images. Instead of producing them both at the correct resolution 750x750 (375 * 2), the first one is saved with the resolution 1000x936 (500x468 * 2), while the second one is saved with the correct size. This is because in some way the SetSize fails for some reason.

I apologize if this is not a very good place to ask about this, but I decided to search after I tracked this to the wx layer because I do not have very much experience with wxpython. I don’t expect people to dig through the mayavi/tvtk/pyface codebases, but if anyone can help diagnose why SetSize might fail proximally inside wx.CallAfter I would greatly appreciate it.

thanks,
R

Roan LaPlante wrote:

In my use case, I create a Mayavi window inside a wx GUI (which
happens to be created by traitsui but my example as follows bypasses
the traitsui component of the problem). Then, using
pyface.GUI.invoke_later() to immediately invoke some GUI events
following the construction of the GUI and invocation of the event
loop, I save some screen captures from mayavi, which under normal
conditions resizes the window. However, in this use case, the window
did not resize properly, and I have tracked the bug down to the call
to SetSize( ) on the wxVTKRenderWindowInteractor which extends
wx.GLCanvas.
...
Here is a minimal snippet which reliably reproduces the problem:

...
def invoke_script(scene=None):
    # pass some script to this function and then execute it

    #_vtk_control is a wxVTKRenderWindowInteractor which is a subclass
of wx.GLCanvas
    scene.scene.scene_editor._vtk_control.SetSize([375,375])
    mlab.savefig('test_invokelater_1.png', figure=scene, magnification=2)
    scene.scene.scene_editor._vtk_control.SetSize([500,468]) #set back
to the original size
   
    #this is equivalent to mlab.savefig('test_invokelater_1.png',
size=(750,750), figure=scene), except the window
    #resizing that is done in several levels of library calls
eventually culminating in a wx canvas is managed manually.

Are you sure the SetSize is failing? Do you get an error returned?

GUI operations like this are all handled by events that have to be
dispatched in an event handler. When you have nested windows and
multiple owners, those events can cause other events to propagate around
the window tree. So, it's not impossible that the size request has not
been completely handled by the time you get control back. As a
superstitious step, you might try inserting a time.sleep(0.1) after the
SetSize to see if it makes a difference. If it does, then there will be
a more deterministic way to wait for it to complete.

If it's actually failing, then my suggestion is just nonsense.

···

--
Tim Roberts, timr@probo.com
Providenza & Boekelheide, Inc.