1 # SPDX-FileCopyrightText: 2011-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 Even though this is in a package this can run as a stand alone scripts.
9 blender --python release/scripts/addons/system_demo_mode/demo_mode.py
11 looks for demo.py textblock or file in the same path as the blend:
14 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'),
15 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'),
18 /data/src/blender/lib/tests/rendering/
25 from . import DEMO_CFG
27 # populate from script
28 global_config_files
= []
33 anim_screen_switch
=0.0,
40 # switch to the next file in 2 sec.
41 global_config_fallback
= dict(
44 anim_screen_switch
=0.0,
55 "anim_cycles": 0, # count how many times we played the anim
58 "render_time": "", # time render was finished.
60 "basedir": "", # demo.py is stored here
66 # -----------------------------------------------------------------------------
67 # render handler - maintain "is_render"
69 def handle_render_clear():
70 for ls
in (bpy
.app
.handlers
.render_complete
, bpy
.app
.handlers
.render_cancel
):
71 while handle_render_done_cb
in ls
:
72 ls
.remove(handle_render_done_cb
)
75 def handle_render_done_cb(self
):
76 global_state
["is_render"] = True
79 def handle_render_init():
81 bpy
.app
.handlers
.render_complete
.append(handle_render_done_cb
)
82 bpy
.app
.handlers
.render_cancel
.append(handle_render_done_cb
)
83 global_state
["is_render"] = False
86 def demo_mode_auto_select():
93 for area
in bpy
.context
.window
.screen
.areas
:
94 size
= area
.width
* area
.height
95 if area
.type in {'VIEW_3D', 'GRAPH_EDITOR', 'DOPESHEET_EDITOR', 'NLA_EDITOR', 'TIMELINE'}:
97 elif area
.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
100 if area
.type == 'IMAGE_EDITOR':
103 # since our test files have this as defacto standard
104 scene
= bpy
.context
.scene
105 if totimg
>= 2 and (scene
.camera
or scene
.render
.use_sequencer
):
108 if play_area
>= render_area
:
119 def demo_mode_next_file(step
=1):
122 if global_config_files
[global_state
["demo_index"]].get("is_tmp"):
123 del global_config_files
[global_state
["demo_index"]]
124 global_state
["demo_index"] -= 1
126 demo_index_next
= (global_state
["demo_index"] + step
) % len(global_config_files
)
128 if global_state
["exit"] and step
> 0:
130 if demo_index_next
< global_state
["demo_index"]:
134 global_state
["demo_index"] = demo_index_next
135 print(global_state
["demo_index"], "....")
136 print("func:demo_mode_next_file", global_state
["demo_index"], "of", len(global_config_files
))
137 filepath
= global_config_files
[global_state
["demo_index"]]["file"]
138 bpy
.ops
.wm
.open_mainfile(filepath
=filepath
)
141 def demo_mode_timer_add():
142 global_state
["timer"] = bpy
.context
.window_manager
.event_timer_add(0.8, window
=bpy
.context
.window
)
145 def demo_mode_timer_remove():
146 if global_state
["timer"]:
147 bpy
.context
.window_manager
.event_timer_remove(global_state
["timer"])
148 global_state
["timer"] = None
151 def demo_mode_load_file():
152 """ Take care, this can only do limited functions since its running
153 before the file is fully loaded.
154 Some operators will crash like playing an animation.
156 print("func:demo_mode_load_file")
157 DemoMode
.first_run
= True
158 bpy
.ops
.wm
.demo_mode('EXEC_DEFAULT')
161 def demo_mode_temp_file():
162 """ Initialize a temp config for the duration of the play time.
163 Use this so we can initialize the demo intro screen but not show again.
165 assert (global_state
["demo_index"] == 0)
167 temp_config
= global_config_fallback
.copy()
168 temp_config
["anim_time_min"] = 0.0
169 temp_config
["anim_time_max"] = 60.0
170 temp_config
["anim_cycles"] = 0 # ensures we switch when hitting the end
171 temp_config
["mode"] = 'PLAY'
172 temp_config
["is_tmp"] = True
174 global_config_files
.insert(0, temp_config
)
177 def demo_mode_init():
178 print("func:demo_mode_init")
179 DemoKeepAlive
.ensure()
182 global_config
.clear()
183 global_config
.update(global_config_files
[global_state
["demo_index"]])
187 demo_mode_timer_add()
189 if global_config
["mode"] == 'AUTO':
190 global_config
["mode"] = demo_mode_auto_select()
192 if global_config
["mode"] == 'PLAY':
193 global_state
["last_frame"] = -1
194 global_state
["anim_cycles"] = 0
195 bpy
.ops
.screen
.animation_play()
197 elif global_config
["mode"] == 'RENDER':
201 scene
= bpy
.context
.scene
202 scene
.render
.filepath
= "TEMP_RENDER"
203 scene
.render
.image_settings
.file_format
= 'AVI_JPEG' if global_config
["anim_render"] else 'PNG'
204 scene
.render
.use_file_extension
= False
205 scene
.render
.use_placeholder
= False
207 # XXX - without this rendering will crash because of a bug in blender!
208 bpy
.ops
.wm
.redraw_timer(type='DRAW_WIN_SWAP', iterations
=1)
209 if global_config
["anim_render"]:
210 bpy
.ops
.render
.render('INVOKE_DEFAULT', animation
=True)
212 bpy
.ops
.render
.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
216 except RuntimeError: # no camera for eg:
218 traceback
.print_exc()
221 raise Exception("Unsupported mode %r" % global_config
["mode"])
223 global_state
["init_time"] = global_state
["last_switch"] = time
.time()
224 global_state
["render_time"] = -1.0
227 def demo_mode_update():
228 time_current
= time
.time()
229 time_delta
= time_current
- global_state
["last_switch"]
230 time_total
= time_current
- global_state
["init_time"]
232 # --------------------------------------------------------------------------
234 if global_config
["mode"] == 'PLAY':
235 frame
= bpy
.context
.scene
.frame_current
237 if time_total
> global_config
["anim_time_max"]:
238 demo_mode_next_file()
240 # above cycles and minimum display time
242 (time_total
> global_config
["anim_time_min"]) and
243 (global_state
["anim_cycles"] > global_config
["anim_cycles"])
246 demo_mode_next_file()
250 if global_state
["reset_anim"]:
251 global_state
["reset_anim"] = False
252 bpy
.ops
.screen
.animation_cancel(restore_frame
=False)
253 bpy
.ops
.screen
.animation_play()
255 # warning, switching the screen can switch the scene
256 # and mess with our last-frame/cycles counting.
257 if global_config
["anim_screen_switch"]:
258 # print(time_delta, 1)
259 if time_delta
> global_config
["anim_screen_switch"]:
260 window
= bpy
.context
.window
262 workspace
= window
.workspace
263 index
= bpy
.data
.workspaces
.keys().index(workspace
.name
)
264 workspace_new
= bpy
.data
.workspaces
[(index
+ 1) % len(bpy
.data
.workspaces
)]
265 window
.workspace
= workspace_new
267 global_state
["last_switch"] = time_current
269 # If we also switch scenes then reset last frame
270 # otherwise it could mess up cycle calculation.
271 if scene
!= window
.scene
:
272 global_state
["last_frame"] = -1
274 # if global_config["mode"] == 'PLAY':
276 global_state
["reset_anim"] = True
279 if global_state
["last_frame"] > frame
:
281 global_state
["anim_cycles"] += 1
283 global_state
["last_frame"] = frame
285 # --------------------------------------------------------------------------
287 elif global_config
["mode"] == 'RENDER':
288 if global_state
["is_render"]:
289 # wait until the time has passed
290 # XXX, todo, if rendering an anim we need some way to check its done.
291 if global_state
["render_time"] == -1.0:
292 global_state
["render_time"] = time
.time()
294 if time
.time() - global_state
["render_time"] > global_config
["display_render"]:
295 handle_render_clear()
296 demo_mode_next_file()
299 raise Exception("Unsupported mode %r" % global_config
["mode"])
301 # -----------------------------------------------------------------------------
306 secret_attr
= "_keepalive"
310 if DemoKeepAlive
.secret_attr
not in bpy
.app
.driver_namespace
:
311 bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
] = DemoKeepAlive()
315 if DemoKeepAlive
.secret_attr
in bpy
.app
.driver_namespace
:
316 del bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
]
319 """ Hack, when the file is loaded the drivers namespace is cleared.
322 demo_mode_load_file()
325 class DemoMode(bpy
.types
.Operator
):
326 bl_idname
= "wm.demo_mode"
332 def cleanup(self
, disable
=False):
333 demo_mode_timer_remove()
334 DemoMode
.first_run
= True
337 DemoMode
.enabled
= False
338 DemoKeepAlive
.remove()
340 def modal(self
, context
, event
):
341 # print("DemoMode.modal", global_state["anim_cycles"])
342 if not DemoMode
.enabled
:
343 self
.cleanup(disable
=True)
346 if event
.type == 'ESC':
347 self
.cleanup(disable
=True)
348 # disable here and not in cleanup because this is a user level disable.
349 # which should stay disabled until explicitly enabled again.
353 if DemoMode
.first_run
:
354 DemoMode
.first_run
= False
360 return {'PASS_THROUGH'}
362 def execute(self
, context
):
363 print("func:DemoMode.execute:", len(global_config_files
), "files")
367 # load config if not loaded
368 if not global_config_files
:
372 if not global_config_files
:
375 "No configuration found with text or file: %s. Run File -> Demo Mode Setup" %
381 demo_mode_temp_file() # play this once through then never again
384 if DemoMode
.enabled
and DemoMode
.first_run
is False:
385 # this actually cancells the previous running instance
386 # should never happen now, DemoModeControl is for this.
389 DemoMode
.enabled
= True
391 context
.window_manager
.modal_handler_add(self
)
392 return {'RUNNING_MODAL'}
394 def cancel(self
, context
):
395 print("func:DemoMode.cancel")
396 # disable here means no running on file-load.
399 # call from DemoModeControl
402 if cls
.enabled
and cls
.first_run
is False:
403 # this actually cancells the previous running instance
404 # should never happen now, DemoModeControl is for this.
408 class DemoModeControl(bpy
.types
.Operator
):
409 bl_idname
= "wm.demo_mode_control"
412 mode
: bpy
.props
.EnumProperty(
414 ('PREV', "Prev", ""),
415 ('PAUSE', "Pause", ""),
416 ('NEXT', "Next", ""),
421 def execute(self
, context
):
424 demo_mode_next_file(-1)
426 demo_mode_next_file(1)
432 def menu_func(self
, context
):
433 # 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"])
435 layout
.operator_context
= 'EXEC_DEFAULT'
436 row
= layout
.row(align
=True)
437 row
.label(text
="Demo Mode:")
438 if not DemoMode
.enabled
:
439 row
.operator("wm.demo_mode", icon
='PLAY', text
="")
441 row
.operator("wm.demo_mode_control", icon
='REW', text
="").mode
= 'PREV'
442 row
.operator("wm.demo_mode_control", icon
='PAUSE', text
="").mode
= 'PAUSE'
443 row
.operator("wm.demo_mode_control", icon
='FF', text
="").mode
= 'NEXT'
447 bpy
.utils
.register_class(DemoMode
)
448 bpy
.utils
.register_class(DemoModeControl
)
449 bpy
.types
.INFO_HT_header
.append(menu_func
)
453 bpy
.utils
.unregister_class(DemoMode
)
454 bpy
.utils
.unregister_class(DemoModeControl
)
455 bpy
.types
.INFO_HT_header
.remove(menu_func
)
458 # -----------------------------------------------------------------------------
461 def load_config(cfg_name
=DEMO_CFG
):
463 global_config_files
.clear()
464 basedir
= os
.path
.dirname(bpy
.data
.filepath
)
466 text
= bpy
.data
.texts
.get(cfg_name
)
468 demo_path
= os
.path
.join(basedir
, cfg_name
)
469 if os
.path
.exists(demo_path
):
470 print("Using config file: %r" % demo_path
)
471 demo_file
= open(demo_path
, "r")
472 demo_data
= demo_file
.read()
477 print("Using config textblock: %r" % cfg_name
)
478 demo_data
= text
.as_string()
479 demo_path
= os
.path
.join(bpy
.data
.filepath
, cfg_name
) # fake
482 print("Could not find %r textblock or %r file." % (DEMO_CFG
, demo_path
))
485 namespace
["__file__"] = demo_path
487 exec(demo_data
, namespace
, namespace
)
489 demo_config
= namespace
["config"]
490 demo_search_path
= namespace
.get("search_path")
491 global_state
["exit"] = namespace
.get("exit", False)
493 if demo_search_path
is None:
494 print("reading: %r, no search_path found, missing files wont be searched." % demo_path
)
495 if demo_search_path
.startswith("//"):
496 demo_search_path
= bpy
.path
.abspath(demo_search_path
)
497 if not os
.path
.exists(demo_search_path
):
498 print("reading: %r, search_path %r does not exist." % (demo_path
, demo_search_path
))
499 demo_search_path
= None
502 # initialize once, case insensitive dict
504 def lookup_file(filepath
):
505 filename
= os
.path
.basename(filepath
).lower()
508 # ensure only ever run once.
509 blend_lookup
[None] = None
511 def blend_dict_items(path
):
512 for dirpath
, dirnames
, filenames
in os
.walk(path
):
514 dirnames
[:] = [d
for d
in dirnames
if not d
.startswith(".")]
515 for filename
in filenames
:
516 if filename
.lower().endswith(".blend"):
517 filepath
= os
.path
.join(dirpath
, filename
)
518 yield (filename
.lower(), filepath
)
520 blend_lookup
.update(dict(blend_dict_items(demo_search_path
)))
522 # fallback to original file
523 return blend_lookup
.get(filename
, filepath
)
524 # done with search lookup
526 for filecfg
in demo_config
:
527 filepath_test
= filecfg
["file"]
528 if not os
.path
.exists(filepath_test
):
529 filepath_test
= os
.path
.join(basedir
, filecfg
["file"])
530 if not os
.path
.exists(filepath_test
):
531 filepath_test
= lookup_file(filepath_test
) # attempt to get from searchpath
532 if not os
.path
.exists(filepath_test
):
533 print("Can't find %r or %r, skipping!")
536 filecfg
["file"] = os
.path
.normpath(filepath_test
)
539 filecfg
["file"] = os
.path
.abspath(filecfg
["file"])
540 filecfg
["file"] = os
.path
.normpath(filecfg
["file"])
541 print(" Adding: %r" % filecfg
["file"])
542 global_config_files
.append(filecfg
)
544 print("found %d files" % len(global_config_files
))
546 global_state
["basedir"] = basedir
548 return bool(global_config_files
)
551 # support direct execution
552 if __name__
== "__main__":
555 demo_mode_load_file() # kick starts the modal operator