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.
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.
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.
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 ?
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
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)
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.
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.
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.
For everyone else, I managed to apply this wonderful advice to another plugin that runs in a modal, here is how you should be able to do it with with all sorts of plugins:
# voxel_heat_diffuse_skinning - the name of the python module, same as the plugin folder found under blender's addon settings.
# VXL_OT_ModalTimerOperator - if you open __init__.py in the plugin folder, it's the name of the class, e.g. class VXL_OT_ModalTimerOperator(...
from voxel_heat_diffuse_skinning import VXL_OT_ModalTimerOperator as op
# some callback function - here we put what shall be run after the modal is finished
def callback(ret):
print('Callback triggered: {} !!'.format(ret))
def modal_wrap(modal_func, callback):
def wrap(self, context, event):
ret, = retset = modal_func(self, context, event)
if ret in {'CANCELLED'}: # my plugin emits the CANCELED event on finish - yours might do FINISH or FINISHED, you might have to look it up in the source code, __init__.py , there look at the modal() function for things like return {'FINISHED'} or function calls that return things alike.
print(f"{self.bl_idname} returned {ret}")
callback(ret)
return retset
return wrap
op._modal_org = op.modal
op.modal = modal_wrap(op.modal, callback)
# and then, simply execute the plugin by its function (found in the init source code assigned to 'bl_idname' with bpy.ops prepended to it, or activate developer mode in python and hover over the UI button that usally triggers the function. For me it is:
bpy.ops.wm.voxel_heat_diffuse()
If you started blender through terminal, you can see there that your callback function is being executed, if you set wrap_modal() to liste to the correct event type your plugin emits ! you can of course also listen to errors, etc…
It’s a bit trashy that the code gets so fragmented through this, the whole flow of my script now has to continue inside the callback function, and I have 3 modal functions to execute…
This “chopping the code to pieces” issue was the main reason I let it go. I had 2 issues where I needed to wait for an operator to finish. One described here where I wanted to use the uv packmaster and then bake a texture on that new uvs. In the other case I subsquently bake serval images after each other and do some compositing on them. Both problems couldn’t be solved without messing everything up so Im gonna wait for Blender to just add an “operator finished” handler …
But where is the difference between a handler and a callback function ? Technically they are the same, a.k.a. some function that is being called upon something ending. Theoretically you could add your own function to the handlers and pass it as an callback to the previously discussed wrapper I don’t see how a handler prevents code fragmentation here ?