Have `modal()` called every frame

I want to implement something similar to the Fly Operator’s behavior where it moves your 3D View forward while holding W and such, but while holding a key I only get a PRESS event at irregular intervals, presumably the character repeat rate you get when holding down a key while typing in a text box. What can I do for my addon to be “woken up” to prepare for the next frame?

1 Like

You can use a timer which triggers a callback to the modal. Then in the modal use context.area.tag_redraw() to update the viewport.

Under invoke

self.timer = context.window_manager.event_timer_add(0.01, window=context.window)

The interval can be 1 / refresh rate or lower.

The timer handler needs to be removed manually when the modal ends.

context.window_manager.event_timer_remove(self.timer)

Alternatively you can use blender’s timer if the modal timer somehow interferes with keypresses (events have a tendency to do that)

Under invoke

bpy.app.timers.register(
    lambda: 0.01 if 'RUNNING_MODAL' in self.modal(context, event) else None
    )

Thanks.

Your first method works, but I don’t get the 120 Hz rate I expect. TIMER events come in at a way faster rate than that.

With your second method, a delay of 0.001 gives me exactly the 120 Hz refresh rate I have set for my screen, but I had to change a fair bit from your suggestion. Having modal() called with an arbitrary event isn’t quite useful, so I made a separate method. There is a bug though. When the operator instance is removed after modal() returns {"CANCELLED"} or {"FINISHED"}, this will cause an error when the timer system attempts to call the method:

Traceback (most recent call last):
  File "C:\Users\Zyl\AppData\Roaming\Blender Foundation\Blender\2.80\scripts\addons\foo.py", line 139, in <lambda>
    bpy.app.timers.register(lambda: self.myFrameUpdate(pinnedStopSignal))
  File "C:\Program Files\Blender Foundation\Blender\2.80\scripts\modules\bpy_types.py", line 670, in __getattribute__
    properties = StructRNA.path_resolve(self, "properties")
ReferenceError: StructRNA of type FooOperator has been removed

Using a global function with a signalling parameter, e.g. [False] (must be some sort of object so it’s actually a reference) can then be used to fan out within the exact instance of the function which was registered. Care needs to be taken to pin the reference. Python Lambdas will use whatever the current value is.

In invoke():

self.stopSignal = [False] # Must be a reference. Here, I use a list with one element.
pinnedStopSignal = self.stopSignal # Avoid race with another call to invoke().
bpy.app.timers.register(lambda: myFrameUpdate(pinnedStopSignal))

Function in global scope:

def myFrameUpdate(stopSignal):
    if stopSignal[0]: return None
    # Do something here
    return 0.001

In modal(), when you want to stop:

self.stopSignal[0] = True
return {"FINISHED"}

Of course this only works when the operator is already guarded against multiple parallel invocations, but when it does, it does work well. Quite abominable, but the best behavior I was able to manage.

Keep in mind also that events in Blender are broadcast globally, and if not consumed it could be something else using a timer that your modal operator is responding to (assuming you’re getting more than 120 timer events per second)

So I finally had time to explore this further. I ran into the problem that the context object is butchered during the call.

    for area in context.screen.areas:
AttributeError: 'NoneType' object has no attribute 'areas'

That kind of makes sense, since we are running code which is decoupled from invoke() and modal(). I was able to fix this by getting what I needed from the context before, and then provide that directly. Also, apparantly Python Lambdas are evaluated as a whole.

This fails:

bpy.app.timers.register(lambda: fpsMove(self, getRegionView3dFromContext(context), pinnedStopSignal))

But this works:

rv3d = getRegionView3dFromContext(context)
bpy.app.timers.register(lambda: fpsMove(self, rv3d, pinnedStopSignal))