How to handle interactions with vtkPlaybackWidget?

So I’m trying to use vtkPlaybackWidget to control animation in a scene, the widget is fairly obscure and I can’t find much documentation on its use. It looks like its representation vtkPlaybackRepresentation is intended to be subclassed and it has dummy functions for each of the icons in the widget but then when the widget is clicked in the renderer nothing happens. The subclassed functions seem to work when explicitly called (e.g. playback_widget.GetRepresentation().Play() ) but there doesn’t seem to be any handling from within the widget.

I added an observer to the widget for ‘AnyEvent’, it registers ‘CursorChangedEvent’ when hovering over the widget, which coincides with the state of the vtkPlaybackRepresentation changing from 0 to 1, clicking registers the events ‘StartInteractionEvent, InteractionEvent, EndInteractionEvent’ but none of the functions in the representation ever get called.

I guess I could grab the screen space coordinates of the click and then translate that into the button positions to then handle the playback, but that seems archaic. Is there meant to be a better option with this widget?

Also there appears to be two small boxes in the 2nd and 5th icons, are these supposed to display anything, are they accessible in the representation?

Many thanks.

It is kind of obscure. I couldn’t find examples. You can study the unit test - https://gitlab.kitware.com/vtk/vtk/-/blob/v9.3.0/Interaction/Widgets/Testing/Cxx/TestPlaybackWidget.cxx

Looks like you’d have to subclass the vtkPlaybackRepresentation and override the playback methods.

So I did try to subclass the representation similar to the C++ test, and if I explicitly call the overridden functions like Play() with playback_widget.GetRepresentation().Play() the overridden functions behave properly, but clicking the buttons in the widget don’t seem to call their respective functions.

It might be a bug with the cocoa renderer on MacOS, or specific to Python. I’ll try to make a minimum working example and see if it behaves differently on another system.

So I made the following minimum example to test the widget in Python, maybe I’m missing something obvious.

The code subclasses vtkPlaybackRepresentation and tests that the widget can access the new overridden functions, I then had to add an observer that then calculates the cursor position and then calls a function depending on the fractional position of the cursor. The inbuilt methods don’t seem to work and I can’t seem to get the bounds of the representation so I guessed the rough ones (super hacky, hate it).

At this point this widget seems about as useful as the textured button, shouldn’t there be an internal method that does all of this in the widget itself?

Looking at the source code it’s definitely there, but maybe the function is no longer used in the Python implementation.



import vtk

class newRepresentation(vtk.vtkPlaybackRepresentation):

    def __init__(self):

        self.SetShowBorderToOff()
        self.ProportionalResizeOn()
        self.BuildRepresentation()
        self.Modified()
        print('Initialised new representation')
        # print(self)

    def Play(self):
        print('Play')
    
    def Stop(self):
        print('Stop')

    def ForwardOneFrame(self):
        print('Forward 1')
 
    def BackwardOneFrame(self):
        print('Backward 1')
 
    def JumpToBeginning(self):
        print('Jump to Beginning')
 
    def JumpToEnd(self):
        print('Jump to End')


def add_playback_widget(render_window_interactor):






    # Create the playback widget
    playback_widget = vtk.vtkPlaybackWidget()
    
    # Set up the playback widget
    playback_widget.SetInteractor(render_window_interactor)

    playback_rep = newRepresentation()
    playback_widget.SetRepresentation(playback_rep)

    playback_widget.PickingManagedOn()
    playback_widget.ProcessEventsOn()
    playback_widget.SetEnabled(True)
    playback_widget.DebugOn()
    playback_widget.On()

    playback_widget.Modified()


    print('Testing overidden functions:')
    playback_widget.GetRepresentation().Play()
    playback_widget.GetRepresentation().Stop()
    playback_widget.GetRepresentation().ForwardOneFrame()
    playback_widget.GetRepresentation().BackwardOneFrame()
    playback_widget.GetRepresentation().JumpToBeginning()
    playback_widget.GetRepresentation().JumpToEnd()
    print('End of tests \n')

    print(playback_rep, sorted(dir(playback_rep)))
    print(playback_widget, sorted(dir(playback_widget)))

    return playback_widget



def click_to_function(playback_widget):


    x_full, _ = playback_widget.GetInteractor().GetEventPosition()

    selection_pos = playback_widget.GetRepresentation().GetSelectionPoint()
    widget_bounds = playback_widget.GetRepresentation().GetBounds()

    print('Any reason these methods don\'t work?', selection_pos, widget_bounds)


    # Hacky workaround to get the fractional position, will break if the widget is moved

    window_size = playback_widget.GetInteractor().GetRenderWindow().GetSize()
    widget_bounds = [0.1 * window_size[0],0.5 * window_size[0], 0.05 * window_size[1], 0.1 * window_size[1]]
    x = (x_full - widget_bounds[0])/(widget_bounds[1]-widget_bounds[0])


    # This seems like a dumb way to do things, shouldn't this all be handled internally?
        
    if (x < 0.16667):
        playback_widget.GetRepresentation().JumpToBeginning()
    elif (x <= 0.333333):
        playback_widget.GetRepresentation().BackwardOneFrame()
    elif (x <= 0.500000):
        playback_widget.GetRepresentation().Stop()
    elif (x < 0.666667):
        playback_widget.GetRepresentation().Play()
    elif (x <= 0.833333):
        playback_widget.GetRepresentation().ForwardOneFrame()
    elif (x <= 1.00000):
        playback_widget.GetRepresentation().JumpToEnd()
    
    return

def playback_callback(widget, event):

    print('Current event:', event)

    state = widget.GetRepresentation().GetInteractionState()
    state_min = widget.GetRepresentation().GetInteractionStateMinValue()
    state_max = widget.GetRepresentation().GetInteractionStateMaxValue()

    print("Representation state", state, state_min, state_max)

    click_to_function(widget)



def main(record_events=False, print_events=True):

    
    # Create a renderer, render window, and interactor
    renderer = vtk.vtkRenderer()
    render_window = vtk.vtkRenderWindow()
    render_window.AddRenderer(renderer)
    render_window_interactor = vtk.vtkRenderWindowInteractor()
    render_window_interactor.SetRenderWindow(render_window)
    render_window_interactor.Initialize()

    # Create a sphere
    sphere_source = vtk.vtkSphereSource()
    sphere_source.SetRadius(5.0)
    sphere_source.SetThetaResolution(30)
    sphere_source.SetPhiResolution(30)
    
    # Create a mapper
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputConnection(sphere_source.GetOutputPort())
    
    # Create an actor
    actor = vtk.vtkActor()
    actor.SetMapper(mapper)
    
    # Add the actor to the scene
    renderer.AddActor(actor)
    renderer.SetBackground(0, 0, 1)

    playback_widget = add_playback_widget(render_window_interactor)

    if print_events:
        print('Code will show any event that happens to the widget\n')
        playback_widget.AddObserver("EndInteractionEvent", playback_callback)

    if record_events:
        print('Code will store every event that happens in the renderer\n')
        recorder = vtk.vtkInteractorEventRecorder()
        recorder.SetInteractor(render_window_interactor)
        recorder.SetFileName("EventRecording.log")
        recorder.SetEnabled(True)
        recorder.Record()

    # Start the interaction
    render_window.Render()

    render_window_interactor.Start()

    if record_events:
        recorder.Stop()
        recorder.Off()

    print('Done')

if __name__ == "__main__":
    main()

The little boxes below the 2nd and 5th icons remain a mystery

You cannot override C++ functions like that from Python.VTK’s wrapping infrastructure doesn’t support it without the @override decorator on the class. This is why the calls to play, pause, etc are not invoking your python functions.

Also there is no separate implementation of VTK in python. The c++ classes directly get wrapped into Python.

Looks like the widget needs to emit an event instead of calling the representation’s functions.