2024-10-25 02:47:11 +00:00
|
|
|
"""Fabex 'async_op.py'
|
2024-04-15 14:02:58 +00:00
|
|
|
|
|
|
|
Functions and Classes to allow asynchronous updates.
|
|
|
|
Used to report progress during path calculation.
|
|
|
|
"""
|
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
import sys
|
|
|
|
|
2024-04-02 15:07:14 +00:00
|
|
|
import bpy
|
|
|
|
|
2024-12-17 19:06:08 +00:00
|
|
|
from ..utilities.async_utils import progress_async
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
class AsyncCancelledException(Exception):
|
|
|
|
pass
|
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
class AsyncOperatorMixin:
|
2025-03-28 14:52:29 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
2024-03-21 12:29:30 +00:00
|
|
|
self.timer = None
|
|
|
|
self.coroutine = None
|
|
|
|
self._is_cancelled = False
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
def modal(self, context, event):
|
2024-09-08 12:15:09 +00:00
|
|
|
"""Handle modal operations for a Blender event.
|
|
|
|
|
|
|
|
This function processes events in a modal operator. It checks for
|
|
|
|
specific event types, such as TIMER and ESC, and performs actions
|
|
|
|
accordingly. If the event type is TIMER, it attempts to execute a tick
|
|
|
|
function, managing the timer and status text in the Blender workspace.
|
|
|
|
If an exception occurs during the tick execution, it handles the error
|
|
|
|
gracefully by removing the timer and reporting the error. The function
|
|
|
|
also allows for cancellation of the operation when the ESC key is
|
|
|
|
pressed.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
context (bpy.context): The current Blender context.
|
|
|
|
event (bpy.types.Event): The event being processed.
|
|
|
|
"""
|
|
|
|
|
2024-01-26 15:02:54 +00:00
|
|
|
if bpy.app.background:
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"PASS_THROUGH"}
|
2024-01-26 15:02:54 +00:00
|
|
|
|
2024-10-25 02:47:11 +00:00
|
|
|
if event.type == "TIMER":
|
2024-01-10 11:15:10 +00:00
|
|
|
try:
|
|
|
|
if self.tick(context):
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"RUNNING_MODAL"}
|
2024-01-10 11:15:10 +00:00
|
|
|
else:
|
|
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
|
|
bpy.context.workspace.status_text_set(None)
|
2024-10-29 20:58:51 +00:00
|
|
|
context.window_manager.progress = 0
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"FINISHED"}
|
2024-01-10 11:15:10 +00:00
|
|
|
except Exception as e:
|
|
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
|
|
bpy.context.workspace.status_text_set(None)
|
2024-10-25 02:47:11 +00:00
|
|
|
self.report({"ERROR"}, str(e))
|
|
|
|
return {"FINISHED"}
|
|
|
|
elif event.type == "ESC":
|
2024-03-21 12:29:30 +00:00
|
|
|
self._is_cancelled = True
|
2024-01-10 11:15:10 +00:00
|
|
|
self.tick(context)
|
|
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
|
|
bpy.context.workspace.status_text_set(None)
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"FINISHED"}
|
|
|
|
if "BLOCKING" in self.bl_options:
|
|
|
|
return {"RUNNING_MODAL"}
|
2024-01-10 11:15:10 +00:00
|
|
|
else:
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"PASS_THROUGH"}
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
def show_progress(self, context, text, n, value_type):
|
2024-09-08 12:15:09 +00:00
|
|
|
"""Display the progress of a task in the workspace and console.
|
|
|
|
|
|
|
|
This function updates the status text in the Blender workspace to show
|
|
|
|
the current progress of a task. It formats the progress message based on
|
|
|
|
the provided parameters and outputs it to both the Blender interface and
|
|
|
|
the standard output. If the value of `n` is not None, it includes the
|
|
|
|
formatted number and value type in the progress message; otherwise, it
|
|
|
|
simply displays the provided text.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
context: The context in which the progress is displayed (typically
|
|
|
|
the Blender context).
|
|
|
|
text (str): A message indicating the task being performed.
|
|
|
|
n (float or None): The current progress value to be displayed.
|
|
|
|
value_type (str): A string representing the type of value (e.g.,
|
|
|
|
percentage, units).
|
|
|
|
"""
|
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
if n is not None:
|
2024-01-10 14:29:44 +00:00
|
|
|
progress_text = f"{text}: {n:.2f}{value_type}"
|
2024-10-29 20:58:51 +00:00
|
|
|
context.window_manager.progress = n * 0.01
|
|
|
|
[a.tag_redraw() for a in context.screen.areas if a.type == "VIEW_3D"]
|
2024-01-10 11:15:10 +00:00
|
|
|
else:
|
|
|
|
progress_text = f"{text}"
|
2024-01-10 14:29:44 +00:00
|
|
|
bpy.context.workspace.status_text_set(progress_text + " (Press ESC to cancel)")
|
2024-01-10 11:15:10 +00:00
|
|
|
sys.stdout.write(f"Progress: {progress_text}\n")
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
def tick(self, context):
|
2024-09-08 12:15:09 +00:00
|
|
|
"""Execute a tick of the coroutine and handle its progress.
|
|
|
|
|
|
|
|
This method checks if the coroutine is initialized; if not, it
|
|
|
|
initializes it by calling `execute_async` with the provided context. It
|
|
|
|
then attempts to send a signal to the coroutine to either continue its
|
|
|
|
execution or handle cancellation. If the coroutine is cancelled, it
|
|
|
|
raises a `StopIteration` exception. The method also processes messages
|
|
|
|
from the coroutine, displaying progress or other messages as needed.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
context: The context in which the coroutine is executed.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: True if the tick was processed successfully, False if the coroutine has
|
|
|
|
completed.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
StopIteration: If the coroutine has completed its execution.
|
|
|
|
Exception: If an unexpected error occurs during the execution of the tick.
|
|
|
|
"""
|
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
if self.coroutine == None:
|
|
|
|
self.coroutine = self.execute_async(context)
|
2024-01-10 11:15:10 +00:00
|
|
|
try:
|
|
|
|
if self._is_cancelled:
|
2024-04-11 19:06:21 +00:00
|
|
|
(msg, args) = self.coroutine.send(AsyncCancelledException("Cancelled with ESC Key"))
|
2024-01-10 11:15:10 +00:00
|
|
|
raise StopIteration
|
|
|
|
else:
|
2024-03-21 12:29:30 +00:00
|
|
|
(msg, args) = self.coroutine.send(None)
|
2024-12-20 17:44:21 +00:00
|
|
|
if msg == "Progress:":
|
2024-03-21 12:29:30 +00:00
|
|
|
self.show_progress(context, **args)
|
2024-01-10 11:15:10 +00:00
|
|
|
else:
|
|
|
|
sys.stdout.write(f"{msg},{args}")
|
|
|
|
return True
|
|
|
|
except StopIteration:
|
|
|
|
return False
|
2024-01-26 15:02:54 +00:00
|
|
|
except Exception as e:
|
2024-04-11 19:06:21 +00:00
|
|
|
print("Exception Thrown in Tick:", e)
|
2024-01-10 11:15:10 +00:00
|
|
|
|
|
|
|
def execute(self, context):
|
2024-09-08 12:15:09 +00:00
|
|
|
"""Execute the modal operation based on the context.
|
|
|
|
|
|
|
|
This function checks if the application is running in the background. If
|
|
|
|
it is, it continuously ticks until the operation is complete. If not, it
|
|
|
|
sets up a timer for the modal operation and adds the modal handler to
|
|
|
|
the window manager, allowing the operation to run in a modal state.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
context (bpy.types.Context): The context in which the operation is executed.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
dict: A dictionary indicating the status of the operation, either
|
|
|
|
{'FINISHED'} if completed or {'RUNNING_MODAL'} if running in modal.
|
|
|
|
"""
|
|
|
|
|
2024-01-10 16:06:13 +00:00
|
|
|
if bpy.app.background:
|
|
|
|
# running in background - don't run as modal,
|
|
|
|
# otherwise tests all fail
|
2024-03-21 12:29:30 +00:00
|
|
|
while self.tick(context) == True:
|
2024-01-10 16:06:13 +00:00
|
|
|
pass
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"FINISHED"}
|
2024-01-10 16:06:13 +00:00
|
|
|
else:
|
2024-10-25 02:47:11 +00:00
|
|
|
self.timer = context.window_manager.event_timer_add(0.001, window=context.window)
|
2024-01-10 16:06:13 +00:00
|
|
|
context.window_manager.modal_handler_add(self)
|
2024-10-25 02:47:11 +00:00
|
|
|
return {"RUNNING_MODAL"}
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
|
|
|
|
class AsyncTestOperator(bpy.types.Operator, AsyncOperatorMixin):
|
2024-04-11 19:06:21 +00:00
|
|
|
"""Test Async Operator"""
|
2024-10-25 02:47:11 +00:00
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
bl_idname = "object.cam_async_test_operator"
|
2024-04-11 19:06:21 +00:00
|
|
|
bl_label = "Test Operator for Async Stuff"
|
2024-10-25 02:47:11 +00:00
|
|
|
bl_options = {"REGISTER", "UNDO", "BLOCKING"}
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
async def execute_async(self, context):
|
2024-09-08 12:15:09 +00:00
|
|
|
"""Execute an asynchronous operation with a progress indicator.
|
|
|
|
|
|
|
|
This function runs a loop 100 times, calling an asynchronous function to
|
|
|
|
report progress for each iteration. It is designed to be used in an
|
|
|
|
asynchronous context where the progress of a task needs to be tracked
|
|
|
|
and reported.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
context: The context in which the asynchronous operation is executed.
|
|
|
|
"""
|
|
|
|
|
2024-01-10 11:15:10 +00:00
|
|
|
for x in range(100):
|
2024-03-21 12:29:30 +00:00
|
|
|
await progress_async("Async test:", x)
|
2024-01-10 11:15:10 +00:00
|
|
|
|
2024-10-25 02:47:11 +00:00
|
|
|
|
2024-03-21 12:29:30 +00:00
|
|
|
# bpy.utils.register_class(AsyncTestOperator)
|