Wait for operator to finish

I have looked up and down the internet but couldn’t find a solution.
My issue is the same as here : https://blender.stackexchange.com/questions/143088/subscribe-to-finished-event-of-modal-operator-and-get-result-of-the-operator/199096?noredirect=1#comment333584_199096

I wan’t to figure out if a modal operator is finished. Simply checking the return value of the operator always gives me “RUNNING MODAL” but never finishes. Macros don’t seem to work, and I also tried using bpy.context.view_layer.update(). I don’t get why it’s so difficult to communicate with an operator or how to run a function that is defined inside it, nor the ability to pass a simple callback function like in js.

Help would be much appreciated

the short answer is that there is not a way to do this.

Oh how I wish we could subscribe to specific/individual operators. Even better if there was a way to know what arguments were passed to said operator when it ran.

Could it be possible that my while loop, that is checking if the operator finished, is blocking the operator to actually finish ? Would it be usefull to run that while loop in a thread, so I can wait for that thread to finish ?

There must be a solution on how to wait for a modal operator to finish. Say you want to create a cube, let the user position it and then keep on with you script doing other fancy stuff to that damn cube.

Hi :slight_smile:

Yes

Yes, but if you want to proceed with other actions, once finished, I would advice not to use a “Thread” from threading library. This would work only for “pure python” actions. If you need bpy actions, most of them will induce hard crash if not on main thread.


Therefore, one way to run another function on main thread is to use a timer :
bpy.app.timers.register(your_function)
It would run as in a while loop.
Basically the function your_function will be called once, and it should return a delay for next call.
So basically your function should be something like :

def your_function():
    if ops_is_running():
        # Wait 0.1 seconds
        return 0.1
    else:
        # Do something
        do_something()
        # Quit the loop
        return None
  • Assuming that ops_is_running is a custom function of your choice where you can check either or not a specific task is running or not (Operator… Or whatever)
  • Returning None is the way to automatically unregister the function from the timer.

See you :slight_smile: ++
Tricotou

First of all, thanks for the reply, I’m glad someone brings up a possible solution for my problem.

Using your solution straight away will produce the polling function of the operator to fail because the bpy.app.timers.register(your_function) will run right away. If using a second parameter, it will be run after a period of time. Both not what I need so I looked up the queued version here :

import bpy
import queue

execution_queue = queue.Queue()

# This function can savely be called in another thread.
# The function will be executed when the timer runs the next time.
def run_in_main_thread(function):
    execution_queue.put(function)

def execute_queued_functions():
    while not execution_queue.empty():
        function = execution_queue.get()
        function()
    return 1.0

bpy.app.timers.register(execute_queued_functions)

Does this mean the app.timer checks every 1 second if something changed in the queue and then executes it ?

Isn’t this checking giving a bad performance ?

So I tried the way with the queue but no luck

def unwrap_async(self):

        basic_functions.run_in_main_thread(self.unwrap_selected)

        basic_functions.run_in_main_thread(bpy.ops.uvpackmaster2.uv_pack)

        basic_functions.run_in_main_thread(bpy.ops.object.editmode_toggle)

    def unwrap_selected(self):

        if self.bake_settings.unwrap:

            self.O.object.add_uv(uv_name=self.bake_settings.uv_name)

            # apply scale on linked

            sel_objects = self.C.selected_objects

            scene_objects = self.D.objects

            linked_objects = set()

            for sel_obj in sel_objects:

                for scene_obj in scene_objects:

                    if sel_obj.data.original is scene_obj.data and sel_obj is not scene_obj:

                        linked_objects.add(sel_obj)

            
            if not len(linked_objects)>0:

                bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

            self.O.object.mode_set(mode='EDIT')

            self.O.mesh.reveal()

            self.O.mesh.select_all(action='SELECT')

            self.O.uv.smart_project(island_margin=self.bake_settings.unwrap_margin)

If the operator you’re trying to get results from is written in python, you can wrap its modal method and add a callback when it returns either FINISHED or CANCELLED.

But this means I need to change the source code of the external Addon, right? I don’t want to change someone else’s source code because other people want to use my add-on without that patched version of his add on

No. The change would happen at runtime.

You would only have to know where the operator is defined, which is either in the module it comes from, or from bpy.types.

import bpy

def modal_wrap(modal_func):

    def wrap(self, context, event):
        ret, = retset = modal_func(self, context, event)
        if ret in {'FINISHED', 'CANCELLED'}:
            print(f"{self.bl_idname} returned {ret}")
        return retset
    return wrap

from some_module import SOME_OT_operator as op

op._modal_org = op.modal
op.modal = modal_wrap(op.modal)

Ok, I have to understand it first. So your are overriding the original modal of that operator with the new one called modal_wrap. Then you feed that modal_wrap with the original modal. Now inside that wrap function you are calling the original modal passing self, context and event you get from where ?

I tried to replace the operator

from uvpackmaster2 import UVP2_OT_PackOperator as op

and it worked !

But I don’t know how to add a callback ?

from uvpackmaster2 import UVP2_OT_PackOperator as op

def modal_wrap(modal_func):

    def wrap(self, context, event,cb):

        ret, = retset = modal_func(self, context, event)

        if ret in {'FINISHED', 'CANCELLED'}:

            print(f"{self.bl_idname} returned {ret}")

            cb()

        return retset

    return wrap

op._modal_org = op.modal

op.modal = modal_wrap(op.modal)

Then call it via

   def test(self):

        print("test")

    def unwrap_async(self):

        bpy.ops.uvpackmaster2.uv_pack(cb = self.test)

Seems wrong :wink: So do I need to add another property to the operator that is a function ?

Sorry for the stupid questions, in JS everything is way easier :wink:

If you know js you might be familiar with decorators, which is what this is. A function which takes a function as argument and produces a new function.

We don’t need to pass arguments to the inner function because we’re not not the ones who are going to call it - blender is (when the operator is called).

You call the uv pack operator normally, and when the operator ends, anything after:

        if ret in {'FINISHED', 'CANCELLED'}:
            print(f"{self.bl_idname} returned {ret}")

in the wrap function would be up to you to do whatever you want. Because that’s literally the point where the operator ends.

How do you imagine the execution chain happens?
Your operator > uvp2 unpack (finishes) > your operator again?

Because if it’s something like this, your operator needs to stay alive until the callback or the wrap function needs to explicitly call your operator with a special argument that would indicate the packing has ended.

Its a bit tricky in my case. I have an operator that calls the bake_texture function that looks like this

def bake_texture(self, selected_objects, bake_settings):

    parent_operator = self

    # ----------------------- CREATE INSTANCE --------------------#

    lightmap_utilities = bake_utilities.BakeUtilities(parent_operator, selected_objects, bake_settings)  

    # -----------------------SET LIGHTMAP UV--------------------#

    lightmap_utilities.set_active_uv_to_lightmap()

    

    # -----------------------SETUP UV'S--------------------#

    lightmap_utilities.unwrap_selected()

    # -----------------------SETUP ENGINE--------------------#

    lightmap_utilities.setup_engine()

    # -----------------------CANGE PREVIEW MODE --------------------#

    bpy.ops.object.preview_bake_texture(connect=False)

    # -----------------------SETUP NODES--------------------#

    lightmap_utilities.add_node_setup()
...

Now here I have the unwrap_selected function in the lightmap_utilities. Right now with your solution, I go into edit mode, pack uv’s and go back in object mode to continue baking and it actrually packs the uv’s now but my chain continues to run so the baking is done while it is still packing the uv’s. My operator doesn’t wait for the packing to be finished and then starts baking.

How would I achieve this ?

I also don’t get this, what is the callback ? I looked up decorators for type script but couldn’t relate to my problem.

The callback is the wrap function above.

I’m assuming unwrap_selected() is basically a call to uvp2?

You cannot wait mid-script because that would impliy a blocking operation. Modal operators get around this by returning early as you’ve likely already noticed.

This means you would need to make your operator into a modal one and stage the execution based on the result.

So, with your operator as modal, execute things in your invoke method until you reach the point where you need to wait for the results of another modal operator (the packing operator). Then send your operator into modal, which waits for the results. When the results are done (eg. a flag was set in the modal wrapper), you exit the modal and call a function which executes the rest of the chain.

I documented the solution from iceythe, in case anyone needs it. At the bottom there is an addon you can donwload and try out.