0
$\begingroup$

The code below runs a modal operator that gets the region_3d.view_distance and prints it out. If you run it you'll notice that the view_distance does not update until the next modal event is detected. It's particularly noticeable when doing 'WHEELUPMOUSE' or 'WHEELDOWNMOUSE' a few times, then a 'MOUSEMOVE'.
You should see 2 'WHEELUPMOUSE' before the first view_distance, and then a last view_distance after moving the mouse slightly.

Is this because the event precedes the action? Is the action (in this case Blender zooming the viewport) only processed by Blender after return {'PASS_THROUGH'}?
If so, how can I get region_3d.view_distance AFTER the viewport has updated and therefor region_3d.view_distance reflects the true view_distance, but during the current model event so I can run other functions that require it?

My goal is to have an object remain the same "on screen" size in the viewport regardless of the zoom level, which works great, it just lags one modal event behind.

import bpy
from bpy.types import Operator


class VIEW_OT_TestModal(Operator):
    bl_idname = "view.test_modal"
    bl_label = "Test Modal"
    bl_description = "Test modal events"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_options = {"BLOCKING"}


    def invoke(self, context, event):
        self.view_distance_last = context.area.spaces.active.region_3d.view_distance
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):

        if event.type == "MOUSEMOVE" or event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE":
            if event.mouse_prev_x != event.mouse_x or event.mouse_prev_y != event.mouse_y:
                print("MOUSEMOVE")
            elif event.type == "WHEELUPMOUSE":
                print("WHEELUPMOUSE")
            elif event.type == "WHEELDOWNMOUSE":
                print("WHEELDOWNMOUSE")

            view_distance = context.area.spaces.active.region_3d.view_distance
            if view_distance != self.view_distance_last:
                print("view_distance", view_distance)
            self.view_distance_last = context.area.spaces.active.region_3d.view_distance

            return {"PASS_THROUGH"}

        elif event.type == "RIGHTMOUSE":
            if event.value == "PRESS":
                return {"FINISHED"}

        return {"RUNNING_MODAL"}


def add_menu(self, context):
    self.layout.operator("view.test_modal", text="Test Modal", icon="AXIS_TOP")


def register():
    bpy.utils.register_class(VIEW_OT_TestModal)
    bpy.types.VIEW3D_MT_view.append(add_menu)


def unregister():
    bpy.utils.unregister_class(VIEW_OT_TestModal)
    bpy.types.VIEW3D_MT_view.remove(add_menu)


if __name__ == "__main__":
    register()
$\endgroup$

1 Answer 1

3
$\begingroup$

Blender doesn't run other default keymap modals until your modal returns something (except you run bpy.ops.()), so the value doesn't update.

enter image description here

import bpy
from bpy.types import Operator


class VIEW_OT_TestModal(Operator):
    bl_idname = "view.test_modal"
    bl_label = "Test Modal"
    bl_description = "Test modal events"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_options = {"BLOCKING"}

    TIMER = None

    def push_modal(self):
        if self.__class__.TIMER is None:
            wm = bpy.context.window_manager
            self.__class__.TIMER = wm.event_timer_add(0, window=bpy.context.window)
    def kill_timer(self):
        if self.__class__.TIMER is not None:
            bpy.context.window_manager.event_timer_remove(self.__class__.TIMER)
            self.__class__.TIMER = None
    def upd_view_distance(self):
        view_distance = bpy.context.area.spaces.active.region_3d.view_distance
        if view_distance != self.view_distance_last:
            print("view_distance", view_distance)
            self.view_distance_last = view_distance

    def invoke(self, context, event):
        self.view_distance_last = context.area.spaces.active.region_3d.view_distance
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if event.type == 'TIMER':
            self.kill_timer()
            self.upd_view_distance()
            return {"RUNNING_MODAL"}

        if event.type in {"WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
            print("mouse wheel")
            self.push_modal()
            return {'PASS_THROUGH'}

        self.upd_view_distance()

        if event.type == "RIGHTMOUSE":
            if event.value == "PRESS":
                return {"FINISHED"}

        return {"RUNNING_MODAL"}


def add_menu(self, context):
    self.layout.operator("view.test_modal", text="Test Modal", icon="AXIS_TOP")


def register():
    bpy.utils.register_class(VIEW_OT_TestModal)
    bpy.types.VIEW3D_MT_view.append(add_menu)


def unregister():
    bpy.utils.unregister_class(VIEW_OT_TestModal)
    bpy.types.VIEW3D_MT_view.remove(add_menu)


if __name__ == "__main__":
    register()
$\endgroup$
5
  • $\begingroup$ So... this creates a TIMER, then when it elapses (if event.type == 'TIMER':) it removes itself and updates view_distance? Then creates a new TIMER on the next mouse event? Right? I think it's the if event.type == 'TIMER': that I'm unsure of. This event occurs when the TIMER elapses? $\endgroup$
    – Psyonic
    Commented Sep 22, 2022 at 5:28
  • 2
    $\begingroup$ Thread only runs when it has mouse/keyboard input events except it has timer events. So add a timer it can keep the Thread looping even without mouse/keyboard event. $\endgroup$
    – X Y
    Commented Sep 22, 2022 at 5:48
  • $\begingroup$ Occasionally when I mess up some code, I think a timer is left running. This leads to a CPU core running at 100% and Blender crashing after a while (I guess it overflows) I can't seem to find where timers stored using the python console so I can remove it. It looks like it should be in D.window_managers['WinMan'] somewhere but I can't find it. Do you know how I can remove a running timer from the python console? $\endgroup$
    – Psyonic
    Commented Sep 23, 2022 at 13:36
  • $\begingroup$ If you need to remove the timer, you need to know the timer location. In above script, you can get the timer from VIEW_OT_TestModal.TIMER. I'm not sure if there is a way to access all running timers, so remember to remove the timers before the modal finishes. $\endgroup$
    – X Y
    Commented Sep 23, 2022 at 15:32
  • $\begingroup$ I am removing the timer in the code, but if I have a bug in my code and it crashes at some point the timer remains in the background. Thanks for your help, I've learned a thing or 3 😊 $\endgroup$
    – Psyonic
    Commented Sep 23, 2022 at 22:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .