Files
blender-portable-repo/scripts/addons/SimpleBake/bake_operators/image_sequence.py
T
2026-07-16 12:41:53 -06:00

136 lines
4.4 KiB
Python

"""Image sequence bake operator.
Drives a frame-by-frame repeat of the main PBR / Cycles bake operator.
Architecture:
- This is a modal operator that owns Blender's event loop for the
duration of the sequence. A 1-second timer polls ``Bip.is_baking``
between frames — this is coarse but reliable across very different
bake durations.
- Because a modal operator needs the event loop, sequence bakes cannot
run under background mode (which fires off a headless subprocess
and exits). The UI disables background mode whenever the image
sequence toggle is on.
- This operator is the single writer of ``Bip.Sequence`` state. All
other modules must consume that state via
``Bip.Sequence.should_run_setup`` / ``should_run_teardown`` rather
than poking the raw flags.
"""
import bpy
from bpy.utils import register_class, unregister_class
from bpy.types import Operator
from bpy.props import StringProperty
from ..background_and_progress import BakeInProgress as Bip
from ..messages import print_message
def _resolve_bpy_op(cmd):
"""Resolve ``"simplebake.foo"`` → ``bpy.ops.simplebake.foo``.
Replaces an older ``eval()``-based call. Raises ValueError if the
operator can't be found or isn't callable.
"""
parts = cmd.split(".")
op = bpy.ops
for part in parts:
op = getattr(op, part, None)
if op is None:
raise ValueError(f"Unknown bpy operator: {cmd}")
if not callable(op):
raise ValueError(f"Resolved object is not callable: {cmd}")
return op
class SimpleBake_OT_Image_Sequence(Operator):
"""SimpleBake Image Sequence Operator"""
bl_idname = "simplebake.image_sequence"
bl_label = "SimpleBake Image Squence Bake Operator"
_timer = []
start_frame = 0
end_frame = 0
cmd: StringProperty()
def modal(self, context, event):
if event.type == 'TIMER':
if Bip.was_error:
print_message(context, "Cancelling Image Sequence timer after error")
self.cancel(context)
return {'CANCELLED'}
cur_frame = context.scene.frame_current
if cur_frame < self.end_frame:
if not Bip.is_baking:
#Must be after the first run
Bip.Sequence.is_first_frame = False
#Check if this will be the last run
if cur_frame + 1 == self.end_frame:
#From here on the per-frame teardown operators will
#run (master restore, copy-and-apply, completion
#message). common_bake_support calls
#Bip.Sequence.reset() once the last frame finishes.
Bip.Sequence.is_last_frame = True
#Advance frame and run the bake again
context.scene.frame_current += 1
print_message(context, f"Image sequence calling main operator for frame {context.scene.frame_current}")
_resolve_bpy_op(self.cmd)()
else:
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def execute(self, context):
sbp = context.scene.SimpleBake_Props
print_message(context, f"Image sequence calling main operator: {self.cmd}")
self.start_frame = sbp.image_sequence_start_frame
self.end_frame = sbp.image_sequence_end_frame
# Inclusive range: we bake at frames start, start+1, ..., end.
Bip.Sequence.active = True
Bip.Sequence.is_first_frame = True
Bip.Sequence.is_last_frame = (self.start_frame == self.end_frame)
Bip.Sequence.total_frames = (self.end_frame - self.start_frame) + 1
context.scene.frame_set(self.start_frame)
wm = context.window_manager
self._timer.append(wm.event_timer_add(1.0, window=context.window))
wm.modal_handler_add(self)
#First run of the chosen command
_resolve_bpy_op(self.cmd)()
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
for t in self._timer:
wm.event_timer_remove(t)
classes = ([
SimpleBake_OT_Image_Sequence
])
def register():
global classes
for cls in classes:
register_class(cls)
def unregister():
global classes
for cls in classes:
unregister_class(cls)