Sun Position: Fix crash when Blender was started in background
[blender-addons.git] / system_demo_mode / demo_mode.py
blobf412869b903722c778df003eb76d2eb12a10d365
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 """
4 Even though this is in a package this can run as a stand alone scripts.
6 # --- example usage
7 blender --python release/scripts/addons/system_demo_mode/demo_mode.py
9 looks for demo.py textblock or file in the same path as the blend:
10 # --- example
11 config = [
12 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'),
13 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'),
15 # ---
16 /data/src/blender/lib/tests/rendering/
17 """
19 import bpy
20 import time
21 import os
23 DEMO_CFG = "demo.py"
25 # populate from script
26 global_config_files = []
28 global_config = dict(
29 anim_cycles=1,
30 anim_render=False,
31 anim_screen_switch=0.0,
32 anim_time_max=60.0,
33 anim_time_min=4.0,
34 mode='AUTO',
35 display_render=4.0,
38 # switch to the next file in 2 sec.
39 global_config_fallback = dict(
40 anim_cycles=1,
41 anim_render=False,
42 anim_screen_switch=0.0,
43 anim_time_max=60.0,
44 anim_time_min=4.0,
45 mode='AUTO',
46 display_render=4.0,
49 global_state = {
50 "init_time": 0.0,
51 "last_switch": 0.0,
52 "reset_anim": False,
53 "anim_cycles": 0, # count how many times we played the anim
54 "last_frame": -1,
55 "is_render": False,
56 "render_time": "", # time render was finished.
57 "timer": None,
58 "basedir": "", # demo.py is stored here
59 "demo_index": 0,
60 "exit": False,
64 # -----------------------------------------------------------------------------
65 # render handler - maintain "is_render"
67 def handle_render_clear():
68 for ls in (bpy.app.handlers.render_complete, bpy.app.handlers.render_cancel):
69 while handle_render_done_cb in ls:
70 ls.remove(handle_render_done_cb)
73 def handle_render_done_cb(self):
74 global_state["is_render"] = True
77 def handle_render_init():
78 handle_render_clear()
79 bpy.app.handlers.render_complete.append(handle_render_done_cb)
80 bpy.app.handlers.render_cancel.append(handle_render_done_cb)
81 global_state["is_render"] = False
84 def demo_mode_auto_select():
86 play_area = 0
87 render_area = 0
89 totimg = 0
91 for area in bpy.context.window.screen.areas:
92 size = area.width * area.height
93 if area.type in {'VIEW_3D', 'GRAPH_EDITOR', 'DOPESHEET_EDITOR', 'NLA_EDITOR', 'TIMELINE'}:
94 play_area += size
95 elif area.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
96 render_area += size
98 if area.type == 'IMAGE_EDITOR':
99 totimg += 1
101 # since our test files have this as defacto standard
102 scene = bpy.context.scene
103 if totimg >= 2 and (scene.camera or scene.render.use_sequencer):
104 mode = 'RENDER'
105 else:
106 if play_area >= render_area:
107 mode = 'PLAY'
108 else:
109 mode = 'RENDER'
111 if 0:
112 return 'PLAY'
114 return mode
117 def demo_mode_next_file(step=1):
119 # support for temp
120 if global_config_files[global_state["demo_index"]].get("is_tmp"):
121 del global_config_files[global_state["demo_index"]]
122 global_state["demo_index"] -= 1
124 print(global_state["demo_index"])
125 demo_index_next = (global_state["demo_index"] + step) % len(global_config_files)
127 if global_state["exit"] and step > 0:
128 # check if we cycled
129 if demo_index_next < global_state["demo_index"]:
130 import sys
131 sys.exit(0)
133 global_state["demo_index"] = demo_index_next
134 print(global_state["demo_index"], "....")
135 print("func:demo_mode_next_file", global_state["demo_index"])
136 filepath = global_config_files[global_state["demo_index"]]["file"]
137 bpy.ops.wm.open_mainfile(filepath=filepath)
140 def demo_mode_timer_add():
141 global_state["timer"] = bpy.context.window_manager.event_timer_add(0.8, window=bpy.context.window)
144 def demo_mode_timer_remove():
145 if global_state["timer"]:
146 bpy.context.window_manager.event_timer_remove(global_state["timer"])
147 global_state["timer"] = None
150 def demo_mode_load_file():
151 """ Take care, this can only do limited functions since its running
152 before the file is fully loaded.
153 Some operators will crash like playing an animation.
155 print("func:demo_mode_load_file")
156 DemoMode.first_run = True
157 bpy.ops.wm.demo_mode('EXEC_DEFAULT')
160 def demo_mode_temp_file():
161 """ Initialize a temp config for the duration of the play time.
162 Use this so we can initialize the demo intro screen but not show again.
164 assert(global_state["demo_index"] == 0)
166 temp_config = global_config_fallback.copy()
167 temp_config["anim_time_min"] = 0.0
168 temp_config["anim_time_max"] = 60.0
169 temp_config["anim_cycles"] = 0 # ensures we switch when hitting the end
170 temp_config["mode"] = 'PLAY'
171 temp_config["is_tmp"] = True
173 global_config_files.insert(0, temp_config)
176 def demo_mode_init():
177 print("func:demo_mode_init")
178 DemoKeepAlive.ensure()
180 if 1:
181 global_config.clear()
182 global_config.update(global_config_files[global_state["demo_index"]])
184 print(global_config)
186 demo_mode_timer_add()
188 if global_config["mode"] == 'AUTO':
189 global_config["mode"] = demo_mode_auto_select()
191 if global_config["mode"] == 'PLAY':
192 global_state["last_frame"] = -1
193 global_state["anim_cycles"] = 0
194 bpy.ops.screen.animation_play()
196 elif global_config["mode"] == 'RENDER':
197 print(" render")
199 # setup scene.
200 scene = bpy.context.scene
201 scene.render.filepath = "TEMP_RENDER"
202 scene.render.image_settings.file_format = 'AVI_JPEG' if global_config["anim_render"] else 'PNG'
203 scene.render.use_file_extension = False
204 scene.render.use_placeholder = False
205 try:
206 # XXX - without this rendering will crash because of a bug in blender!
207 bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
208 if global_config["anim_render"]:
209 bpy.ops.render.render('INVOKE_DEFAULT', animation=True)
210 else:
211 bpy.ops.render.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
213 handle_render_init()
215 except RuntimeError: # no camera for eg:
216 import traceback
217 traceback.print_exc()
219 else:
220 raise Exception("Unsupported mode %r" % global_config["mode"])
222 global_state["init_time"] = global_state["last_switch"] = time.time()
223 global_state["render_time"] = -1.0
226 def demo_mode_update():
227 time_current = time.time()
228 time_delta = time_current - global_state["last_switch"]
229 time_total = time_current - global_state["init_time"]
231 # --------------------------------------------------------------------------
232 # ANIMATE MODE
233 if global_config["mode"] == 'PLAY':
234 frame = bpy.context.scene.frame_current
235 # check for exit
236 if time_total > global_config["anim_time_max"]:
237 demo_mode_next_file()
238 return
239 # above cycles and minimum display time
240 if (
241 (time_total > global_config["anim_time_min"]) and
242 (global_state["anim_cycles"] > global_config["anim_cycles"])
244 # looped enough now.
245 demo_mode_next_file()
246 return
248 # run update funcs
249 if global_state["reset_anim"]:
250 global_state["reset_anim"] = False
251 bpy.ops.screen.animation_cancel(restore_frame=False)
252 bpy.ops.screen.animation_play()
254 # warning, switching the screen can switch the scene
255 # and mess with our last-frame/cycles counting.
256 if global_config["anim_screen_switch"]:
257 # print(time_delta, 1)
258 if time_delta > global_config["anim_screen_switch"]:
259 window = bpy.context.window
260 scene = window.scene
261 workspace = window.workspace
262 index = bpy.data.workspaces.keys().index(workspace.name)
263 workspace_new = bpy.data.workspaces[(index + 1) % len(bpy.data.workspaces)]
264 window.workspace = workspace_new
266 global_state["last_switch"] = time_current
268 # If we also switch scenes then reset last frame
269 # otherwise it could mess up cycle calculation.
270 if scene != window.scene:
271 global_state["last_frame"] = -1
273 #if global_config["mode"] == 'PLAY':
274 if 1:
275 global_state["reset_anim"] = True
277 # did we loop?
278 if global_state["last_frame"] > frame:
279 print("Cycle!")
280 global_state["anim_cycles"] += 1
282 global_state["last_frame"] = frame
284 # --------------------------------------------------------------------------
285 # RENDER MODE
286 elif global_config["mode"] == 'RENDER':
287 if global_state["is_render"]:
288 # wait until the time has passed
289 # XXX, todo, if rendering an anim we need some way to check its done.
290 if global_state["render_time"] == -1.0:
291 global_state["render_time"] = time.time()
292 else:
293 if time.time() - global_state["render_time"] > global_config["display_render"]:
294 handle_render_clear()
295 demo_mode_next_file()
296 return
297 else:
298 raise Exception("Unsupported mode %r" % global_config["mode"])
300 # -----------------------------------------------------------------------------
301 # modal operator
304 class DemoKeepAlive:
305 secret_attr = "_keepalive"
307 @staticmethod
308 def ensure():
309 if DemoKeepAlive.secret_attr not in bpy.app.driver_namespace:
310 bpy.app.driver_namespace[DemoKeepAlive.secret_attr] = DemoKeepAlive()
312 @staticmethod
313 def remove():
314 if DemoKeepAlive.secret_attr in bpy.app.driver_namespace:
315 del bpy.app.driver_namespace[DemoKeepAlive.secret_attr]
317 def __del__(self):
318 """ Hack, when the file is loaded the drivers namespace is cleared.
320 if DemoMode.enabled:
321 demo_mode_load_file()
324 class DemoMode(bpy.types.Operator):
325 bl_idname = "wm.demo_mode"
326 bl_label = "Demo"
328 enabled = False
329 first_run = True
331 def cleanup(self, disable=False):
332 demo_mode_timer_remove()
333 DemoMode.first_run = True
335 if disable:
336 DemoMode.enabled = False
337 DemoKeepAlive.remove()
339 def modal(self, context, event):
340 # print("DemoMode.modal", global_state["anim_cycles"])
341 if not DemoMode.enabled:
342 self.cleanup(disable=True)
343 return {'CANCELLED'}
345 if event.type == 'ESC':
346 self.cleanup(disable=True)
347 # disable here and not in cleanup because this is a user level disable.
348 # which should stay disabled until explicitly enabled again.
349 return {'CANCELLED'}
351 # print(event.type)
352 if DemoMode.first_run:
353 DemoMode.first_run = False
355 demo_mode_init()
356 else:
357 demo_mode_update()
359 return {'PASS_THROUGH'}
361 def execute(self, context):
362 print("func:DemoMode.execute:", len(global_config_files), "files")
364 use_temp = False
366 # load config if not loaded
367 if not global_config_files:
368 load_config()
369 use_temp = True
371 if not global_config_files:
372 self.report({'INFO'}, "No configuration found with text or file: %s. Run File -> Demo Mode Setup" % DEMO_CFG)
373 return {'CANCELLED'}
375 if use_temp:
376 demo_mode_temp_file() # play this once through then never again
378 # toggle
379 if DemoMode.enabled and DemoMode.first_run is False:
380 # this actually cancells the previous running instance
381 # should never happen now, DemoModeControl is for this.
382 return {'CANCELLED'}
383 else:
384 DemoMode.enabled = True
386 context.window_manager.modal_handler_add(self)
387 return {'RUNNING_MODAL'}
389 def cancel(self, context):
390 print("func:DemoMode.cancel")
391 # disable here means no running on file-load.
392 self.cleanup()
394 # call from DemoModeControl
395 @classmethod
396 def disable(cls):
397 if cls.enabled and cls.first_run is False:
398 # this actually cancells the previous running instance
399 # should never happen now, DemoModeControl is for this.
400 cls.enabled = False
403 class DemoModeControl(bpy.types.Operator):
404 bl_idname = "wm.demo_mode_control"
405 bl_label = "Control"
407 mode: bpy.props.EnumProperty(
408 items=(('PREV', "Prev", ""),
409 ('PAUSE', "Pause", ""),
410 ('NEXT', "Next", "")),
411 name="Mode"
414 def execute(self, context):
415 mode = self.mode
416 if mode == 'PREV':
417 demo_mode_next_file(-1)
418 elif mode == 'NEXT':
419 demo_mode_next_file(1)
420 else: # pause
421 DemoMode.disable()
422 return {'FINISHED'}
425 def menu_func(self, context):
426 # 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"])
427 layout = self.layout
428 layout.operator_context = 'EXEC_DEFAULT'
429 row = layout.row(align=True)
430 row.label(text="Demo Mode:")
431 if not DemoMode.enabled:
432 row.operator("wm.demo_mode", icon='PLAY', text="")
433 else:
434 row.operator("wm.demo_mode_control", icon='REW', text="").mode = 'PREV'
435 row.operator("wm.demo_mode_control", icon='PAUSE', text="").mode = 'PAUSE'
436 row.operator("wm.demo_mode_control", icon='FF', text="").mode = 'NEXT'
439 def register():
440 bpy.utils.register_class(DemoMode)
441 bpy.utils.register_class(DemoModeControl)
442 bpy.types.INFO_HT_header.append(menu_func)
445 def unregister():
446 bpy.utils.unregister_class(DemoMode)
447 bpy.utils.unregister_class(DemoModeControl)
448 bpy.types.INFO_HT_header.remove(menu_func)
451 # -----------------------------------------------------------------------------
452 # parse args
454 def load_config(cfg_name=DEMO_CFG):
455 namespace = {}
456 del global_config_files[:]
457 basedir = os.path.dirname(bpy.data.filepath)
459 text = bpy.data.texts.get(cfg_name)
460 if text is None:
461 demo_path = os.path.join(basedir, cfg_name)
462 if os.path.exists(demo_path):
463 print("Using config file: %r" % demo_path)
464 demo_file = open(demo_path, "r")
465 demo_data = demo_file.read()
466 demo_file.close()
467 else:
468 demo_data = ""
469 else:
470 print("Using config textblock: %r" % cfg_name)
471 demo_data = text.as_string()
472 demo_path = os.path.join(bpy.data.filepath, cfg_name) # fake
474 if not demo_data:
475 print("Could not find %r textblock or %r file." % (DEMO_CFG, demo_path))
476 return False
478 namespace["__file__"] = demo_path
480 exec(demo_data, namespace, namespace)
482 demo_config = namespace["config"]
483 demo_search_path = namespace.get("search_path")
484 global_state["exit"] = namespace.get("exit", False)
486 if demo_search_path is None:
487 print("reading: %r, no search_path found, missing files wont be searched." % demo_path)
488 if demo_search_path.startswith("//"):
489 demo_search_path = bpy.path.abspath(demo_search_path)
490 if not os.path.exists(demo_search_path):
491 print("reading: %r, search_path %r does not exist." % (demo_path, demo_search_path))
492 demo_search_path = None
494 blend_lookup = {}
495 # initialize once, case insensitive dict
497 def lookup_file(filepath):
498 filename = os.path.basename(filepath).lower()
500 if not blend_lookup:
501 # ensure only ever run once.
502 blend_lookup[None] = None
504 def blend_dict_items(path):
505 for dirpath, dirnames, filenames in os.walk(path):
506 # skip '.git'
507 dirnames[:] = [d for d in dirnames if not d.startswith(".")]
508 for filename in filenames:
509 if filename.lower().endswith(".blend"):
510 filepath = os.path.join(dirpath, filename)
511 yield (filename.lower(), filepath)
513 blend_lookup.update(dict(blend_dict_items(demo_search_path)))
515 # fallback to original file
516 return blend_lookup.get(filename, filepath)
517 # done with search lookup
519 for filecfg in demo_config:
520 filepath_test = filecfg["file"]
521 if not os.path.exists(filepath_test):
522 filepath_test = os.path.join(basedir, filecfg["file"])
523 if not os.path.exists(filepath_test):
524 filepath_test = lookup_file(filepath_test) # attempt to get from searchpath
525 if not os.path.exists(filepath_test):
526 print("Can't find %r or %r, skipping!")
527 continue
529 filecfg["file"] = os.path.normpath(filepath_test)
531 # sanitize
532 filecfg["file"] = os.path.abspath(filecfg["file"])
533 filecfg["file"] = os.path.normpath(filecfg["file"])
534 print(" Adding: %r" % filecfg["file"])
535 global_config_files.append(filecfg)
537 print("found %d files" % len(global_config_files))
539 global_state["basedir"] = basedir
541 return bool(global_config_files)
544 # support direct execution
545 if __name__ == "__main__":
546 register()
548 demo_mode_load_file() # kick starts the modal operator