Newer
Older
Campbell Barton
committed
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
'''
Even though this is in a package this can run as a stand alone scripts.
# --- example usage
blender --python release/scripts/addons/system_demo_mode/demo_mode.py
looks for demo.py textblock or file in the same path as the blend:
# --- example
config = [
dict(anim_cycles=1, anim_render=False, anim_screen_switch=0.0, anim_time_max=10.0, anim_time_min=4.0, mode='AUTO', display_render=4.0, file='/l/19534_simplest_mesh_2.blend'),
dict(anim_cycles=1, anim_render=False, anim_screen_switch=0.0, anim_time_max=10.0, anim_time_min=4.0, mode='AUTO', display_render=4.0, file='/l/252_pivotConstraint_01.blend'),
Campbell Barton
committed
]
# ---
/data/src/blender/lib/tests/rendering/
Campbell Barton
committed
'''
import bpy
import time
import tempfile
import os
DEMO_CFG = "demo.py"
Campbell Barton
committed
# populate from script
global_config_files = []
global_config = dict(anim_cycles=1,
Campbell Barton
committed
anim_render=False,
anim_screen_switch=0.0,
anim_time_max=60.0,
anim_time_min=4.0,
mode='AUTO',
display_render=4.0)
# switch to the next file in 2 sec.
global_config_fallback = dict(anim_cycles=1,
Campbell Barton
committed
anim_render=False,
anim_screen_switch=0.0,
anim_time_max=60.0,
anim_time_min=4.0,
mode='AUTO',
display_render=4.0)
global_state = {
"init_time": 0.0,
"last_switch": 0.0,
"reset_anim": False,
"anim_cycles": 0, # count how many times we played the anim
"last_frame": -1,
Campbell Barton
committed
"render_out": "",
"render_time": "", # time render was finished.
"timer": None,
"basedir": "", # demo.py is stored here
"demo_index": 0,
}
Campbell Barton
committed
def demo_mode_auto_select():
Campbell Barton
committed
play_area = 0
render_area = 0
Campbell Barton
committed
for area in bpy.context.window.screen.areas:
size = area.width * area.height
if area.type in {'VIEW_3D', 'GRAPH_EDITOR', 'DOPESHEET_EDITOR', 'NLA_EDITOR', 'TIMELINE'}:
play_area += size
elif area.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
render_area += size
if area.type == 'IMAGE_EDITOR':
totimg += 1
# since our test files have this as defacto standard
scene = bpy.context.scene
if totimg >= 2 and (scene.camera or scene.render.use_sequencer):
mode = 'RENDER'
else:
if play_area >= render_area:
mode = 'PLAY'
else:
mode = 'RENDER'
if 0:
return 'PLAY'
return mode
Campbell Barton
committed
def demo_mode_next_file(step=1):
print(global_state["demo_index"])
global_state["demo_index"] = (global_state["demo_index"] + step) % len(global_config_files)
print(global_state["demo_index"], "....")
Campbell Barton
committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
print("func:demo_mode_next_file", global_state["demo_index"])
filepath = global_config_files[global_state["demo_index"]]["file"]
bpy.ops.wm.open_mainfile(filepath=filepath)
def demo_mode_timer_add():
global_state["timer"] = bpy.context.window_manager.event_timer_add(0.8, bpy.context.window)
def demo_mode_timer_remove():
if global_state["timer"]:
bpy.context.window_manager.event_timer_remove(global_state["timer"])
global_state["timer"] = None
def demo_mode_load_file():
""" Take care, this can only do limited functions since its running
before the file is fully loaded.
Some operators will crash like playing an animation.
"""
print("func:demo_mode_load_file")
DemoMode.first_run = True
bpy.ops.wm.demo_mode('EXEC_DEFAULT')
def demo_mode_init():
print("func:demo_mode_init")
DemoKeepAlive.ensure()
if 1:
global_config.clear()
global_config.update(global_config_files[global_state["demo_index"]])
print(global_config)
demo_mode_timer_add()
if global_config["mode"] == 'AUTO':
global_config["mode"] = demo_mode_auto_select()
if global_config["mode"] == 'PLAY':
global_state["last_frame"] = -1
global_state["anim_cycles"] = 0
Campbell Barton
committed
bpy.ops.screen.animation_play()
elif global_config["mode"] == 'RENDER':
print(" render")
handle, global_state["render_out"] = tempfile.mkstemp()
os.close(handle)
del handle
Campbell Barton
committed
if os.path.exists(global_state["render_out"]):
print(" render!!!")
os.remove(global_state["render_out"])
# setup scene.
scene = bpy.context.scene
scene.render.filepath = global_state["render_out"]
scene.render.file_format = 'AVI_JPEG' if global_config["anim_render"] else 'PNG'
scene.render.use_file_extension = False
scene.render.use_placeholder = False
try:
if global_config["anim_render"]:
bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
else:
bpy.ops.render.render('INVOKE_DEFAULT', write_still=True)
except RuntimeError: # no camera for eg:
import traceback
traceback.print_exc()
open(global_state["render_out"], 'w').close() # touch so we move on.
Campbell Barton
committed
else:
raise Exception("Unsupported mode %r" % global_config["mode"])
global_state["init_time"] = global_state["last_switch"] = time.time()
global_state["render_time"] = -1.0
def demo_mode_update():
time_current = time.time()
time_delta = time_current - global_state["last_switch"]
time_total = time_current - global_state["init_time"]
# --------------------------------------------------------------------------
# ANIMATE MODE
if global_config["mode"] == 'PLAY':
frame = bpy.context.scene.frame_current
Campbell Barton
committed
# check for exit
if time_total > global_config["anim_time_max"]:
demo_mode_next_file()
return
# above cycles and minimum display time
if (time_total > global_config["anim_time_min"]) and \
(global_state["anim_cycles"] > global_config["anim_cycles"]):
# looped enough now.
demo_mode_next_file()
return
Campbell Barton
committed
# run update funcs
if global_state["reset_anim"]:
global_state["reset_anim"] = False
bpy.ops.screen.animation_cancel(restore_frame=False)
bpy.ops.screen.animation_play()
# warning, switching the screen can switch the scene
# and mess with our last-frame/cycles counting.
Campbell Barton
committed
if global_config["anim_screen_switch"]:
# print(time_delta, 1)
if time_delta > global_config["anim_screen_switch"]:
screen = bpy.context.window.screen
index = bpy.data.screens.keys().index(screen.name)
screen_new = bpy.data.screens[(index if index > 0 else len(bpy.data.screens)) - 1]
bpy.context.window.screen = screen_new
global_state["last_switch"] = time_current
# if we also switch scenes then reset last frame
# otherwise it could mess up cycle calc.
if screen.scene != screen_new.scene:
global_state["last_frame"] = -1
Campbell Barton
committed
#if global_config["mode"] == 'PLAY':
if 1:
global_state["reset_anim"] = True
# did we loop?
if global_state["last_frame"] > frame:
print("Cycle!")
global_state["anim_cycles"] += 1
global_state["last_frame"] = frame
Campbell Barton
committed
# --------------------------------------------------------------------------
# RENDER MODE
elif global_config["mode"] == 'RENDER':
if os.path.exists(global_state["render_out"]):
# wait until the time has passed
# XXX, todo, if rendering an anim we need some way to check its done.
Campbell Barton
committed
if global_state["render_time"] == -1.0:
global_state["render_time"] = time.time()
else:
if time.time() - global_state["render_time"] > global_config["display_render"]:
os.remove(global_state["render_out"])
demo_mode_next_file()
return
else:
raise Exception("Unsupported mode %r" % global_config["mode"])
# -----------------------------------------------------------------------------
# modal operator
Campbell Barton
committed
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
class DemoKeepAlive:
secret_attr = "_keepalive"
@staticmethod
def ensure():
if DemoKeepAlive.secret_attr not in bpy.app.driver_namespace:
bpy.app.driver_namespace[DemoKeepAlive.secret_attr] = DemoKeepAlive()
@staticmethod
def remove():
if DemoKeepAlive.secret_attr in bpy.app.driver_namespace:
del bpy.app.driver_namespace[DemoKeepAlive.secret_attr]
def __del__(self):
""" Hack, when the file is loaded the drivers namespace is cleared.
"""
if DemoMode.enabled:
demo_mode_load_file()
class DemoMode(bpy.types.Operator):
bl_idname = "wm.demo_mode"
bl_label = "Demo"
enabled = False
first_run = True
def cleanup(self, disable=False):
demo_mode_timer_remove()
DemoMode.first_run = True
Campbell Barton
committed
if disable:
DemoMode.enabled = False
Campbell Barton
committed
DemoKeepAlive.remove()
def modal(self, context, event):
# print("DemoMode.modal", global_state["anim_cycles"])
if not DemoMode.enabled:
Campbell Barton
committed
self.cleanup(disable=True)
return {'CANCELLED'}
if event.type == 'ESC':
self.cleanup(disable=True)
# disable here and not in cleanup because this is a user level disable.
# which should stay disabled until explicitly enabled again.
return {'CANCELLED'}
# print(event.type)
if DemoMode.first_run:
DemoMode.first_run = False
Campbell Barton
committed
demo_mode_init()
else:
demo_mode_update()
return {'PASS_THROUGH'}
def execute(self, context):
print("func:DemoMode.execute:", len(global_config_files), "files")
# load config if not loaded
if not global_config_files:
load_config()
if not global_config_files:
self.report({'INFO'}, "No configuration found with text or file: %s. Run File -> Demo Mode Setup" % DEMO_CFG)
return {'CANCELLED'}
Campbell Barton
committed
# toggle
if DemoMode.enabled and DemoMode.first_run == False:
Campbell Barton
committed
# this actually cancells the previous running instance
# should never happen now, DemoModeControl is for this.
Campbell Barton
committed
return {'CANCELLED'}
else:
DemoMode.enabled = True
Campbell Barton
committed
context.window_manager.modal_handler_add(self)
Campbell Barton
committed
return {'RUNNING_MODAL'}
def cancel(self, context):
print("func:DemoMode.cancel")
# disable here means no running on file-load.
self.cleanup()
Campbell Barton
committed
# call from DemoModeControl
@classmethod
def disable(cls):
if cls.enabled and cls.first_run == False:
# this actually cancells the previous running instance
# should never happen now, DemoModeControl is for this.
cls.enabled = False
class DemoModeControl(bpy.types.Operator):
bl_idname = "wm.demo_mode_control"
bl_label = "Control"
mode = bpy.props.EnumProperty(items=(
('PREV', "Prev", ""),
('PAUSE', "Pause", ""),
('NEXT', "Next", ""),
),
name="Mode")
def execute(self, context):
mode = self.mode
if mode == 'PREV':
demo_mode_next_file(-1)
elif mode == 'NEXT':
demo_mode_next_file(1)
DemoMode.disable()
return {'FINISHED'}
Campbell Barton
committed
def menu_func(self, context):
# print("func:menu_func - DemoMode.enabled:", DemoMode.enabled, "bpy.app.driver_namespace:", DemoKeepAlive.secret_attr not in bpy.app.driver_namespace, 'global_state["timer"]:', global_state["timer"])
layout = self.layout
layout.operator_context = 'EXEC_DEFAULT'
box = layout.row() # BOX messes layout
row = box.row(align=True)
row.label("Demo Mode:")
if not DemoMode.enabled:
row.operator("wm.demo_mode", icon='PLAY', text="")
else:
row.operator("wm.demo_mode_control", icon='REW', text="").mode = 'PREV'
row.operator("wm.demo_mode_control", icon='PAUSE', text="").mode = 'PAUSE'
row.operator("wm.demo_mode_control", icon='FF', text="").mode = 'NEXT'
Campbell Barton
committed
Campbell Barton
committed
def register():
bpy.utils.register_class(DemoMode)
bpy.utils.register_class(DemoModeControl)
Campbell Barton
committed
bpy.types.INFO_HT_header.append(menu_func)
def unregister():
bpy.utils.unregister_class(DemoMode)
bpy.utils.unregister_class(DemoModeControl)
bpy.types.INFO_HT_header.remove(menu_func)
Campbell Barton
committed
# -----------------------------------------------------------------------------
# parse args
def load_config(cfg_name=DEMO_CFG):
Campbell Barton
committed
namespace = {}
global_config_files[:] = []
basedir = os.path.dirname(bpy.data.filepath)
Campbell Barton
committed
text = bpy.data.texts.get(cfg_name)
if text is None:
demo_path = os.path.join(basedir, cfg_name)
if os.path.exists(demo_path):
print("Using config file: %r" % demo_path)
demo_file = open(demo_path, "r")
demo_data = demo_file.read()
demo_file.close()
else:
demo_data = ""
Campbell Barton
committed
else:
print("Using config textblock: %r" % cfg_name)
Campbell Barton
committed
demo_data = text.as_string()
demo_path = os.path.join(bpy.data.filepath, cfg_name) # fake
if not demo_data:
print("Could not find %r textblock or %r file." % (DEMO_CFG, demo_path))
return False
Campbell Barton
committed
namespace["__file__"] = demo_path
exec(demo_data, namespace, namespace)
demo_config = namespace["config"]
demo_search_path = namespace.get("search_path")
if demo_search_path is None:
print("reading: %r, no search_path found, missing files wont be searched." % demo_path)
if demo_search_path.startswith("//"):
demo_search_path = os.path.relpath(demo_search_path)
if not os.path.exists(demo_search_path):
print("reading: %r, search_path %r does not exist." % (demo_path, demo_search_path))
demo_search_path = None
blend_lookup = {}
# initialize once, case insensitive dict
def lookup_file(filepath):
filename = os.path.basename(filepath).lower()
if not blend_lookup:
# ensure only ever run once.
blend_lookup[None] = None
def blend_dict_items(path):
for dirpath, dirnames, filenames in os.walk(path):
# skip '.svn'
if dirpath.startswith("."):
continue
for filename in filenames:
if filename.lower().endswith(".blend"):
filepath = os.path.join(dirpath, filename)
yield (filename.lower(), filepath)
blend_lookup.update(dict(blend_dict_items(demo_search_path)))
# fallback to orginal file
return blend_lookup.get(filename, filepath)
# done with search lookup
Campbell Barton
committed
for filecfg in demo_config:
filepath_test = filecfg["file"]
if not os.path.exists(filepath_test):
Campbell Barton
committed
filepath_test = os.path.join(basedir, filecfg["file"])
if not os.path.exists(filepath_test):
filepath_test = lookup_file(filepath_test) # attempt to get from searchpath
if not os.path.exists(filepath_test):
print("Cant find %r or %r, skipping!")
continue
filecfg["file"] = os.path.normpath(filepath_test)
Campbell Barton
committed
# sanitize
filecfg["file"] = os.path.abspath(filecfg["file"])
filecfg["file"] = os.path.normpath(filecfg["file"])
print(" Adding: %r" % filecfg["file"])
global_config_files.append(filecfg)
print("found %d files" % len(global_config_files))
global_state["basedir"] = basedir
return bool(global_config_files)
Campbell Barton
committed
# support direct execution
if __name__ == "__main__":
register()
demo_mode_load_file() # kick starts the modal operator