diff options
Diffstat (limited to 'system_demo_mode/demo_mode.py')
-rw-r--r-- | system_demo_mode/demo_mode.py | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/system_demo_mode/demo_mode.py b/system_demo_mode/demo_mode.py new file mode 100644 index 00000000..a2e64839 --- /dev/null +++ b/system_demo_mode/demo_mode.py @@ -0,0 +1,510 @@ +# ##### 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'), + ] +# --- +/data/src/blender/lib/tests/rendering/ +''' + +import bpy +import time +import tempfile +import os + +DEMO_CFG = "demo.py" + +# populate from script +global_config_files = [] + + +global_config = dict(anim_cycles=1, + 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, + 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, + "render_out": "", + "render_time": "", # time render was finished. + "timer": None, + "basedir": "", # demo.py is stored here + "demo_index": 0, +} + + +def demo_mode_auto_select(): + + play_area = 0 + render_area = 0 + + totimg = 0 + + 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 + + +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"], "....") + 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 + bpy.ops.screen.animation_play() + + elif global_config["mode"] == 'RENDER': + print(" render") + + # setup tempfile + global_state["render_out"] = tempfile.mkstemp()[1] + 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. + + 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 + # 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 + + # 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. + 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 + + #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 + + # -------------------------------------------------------------------------- + # 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. + 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 + + +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() + __class__.first_run = True + + if disable: + __class__.enabled = False + DemoKeepAlive.remove() + + def modal(self, context, event): + # print("DemoMode.modal", global_state["anim_cycles"]) + if not __class__.enabled: + 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 __class__.first_run: + __class__.first_run = False + + 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'} + + # toggle + if __class__.enabled and __class__.first_run == False: + # this actually cancells the previous running instance + # should never happen now, DemoModeControl is for this. + return {'CANCELLED'} + else: + __class__.enabled = True + context.window_manager.modal_handler_add(self) + + return {'RUNNING_MODAL'} + + def cancel(self, context): + print("func:DemoMode.cancel") + # disable here means no running on file-load. + self.cleanup() + return {'CANCELLED'} + + # 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) + else: # pause + DemoMode.disable() + return {'FINISHED'} + + +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' + + +def register(): + bpy.utils.register_class(DemoMode) + bpy.utils.register_class(DemoModeControl) + 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) + + +# ----------------------------------------------------------------------------- +# parse args + +def load_config(cfg_name=DEMO_CFG): + namespace = {} + global_config_files[:] = [] + basedir = os.path.dirname(bpy.data.filepath) + + 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 = "" + else: + print("Using config textblock: %r" % cfg_name) + 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 + + 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 + + for filecfg in demo_config: + filepath_test = filecfg["file"] + if not os.path.exists(filepath_test): + 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) + + # 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) + + +# support direct execution +if __name__ == "__main__": + register() + + demo_mode_load_file() # kick starts the modal operator |