1 # 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/
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 print(global_state
["demo_index"])
127 demo_index_next
= (global_state
["demo_index"] + step
) % len(global_config_files
)
129 if global_state
["exit"] and step
> 0:
131 if demo_index_next
< global_state
["demo_index"]:
135 global_state
["demo_index"] = demo_index_next
136 print(global_state
["demo_index"], "....")
137 print("func:demo_mode_next_file", global_state
["demo_index"])
138 filepath
= global_config_files
[global_state
["demo_index"]]["file"]
139 bpy
.ops
.wm
.open_mainfile(filepath
=filepath
)
142 def demo_mode_timer_add():
143 global_state
["timer"] = bpy
.context
.window_manager
.event_timer_add(0.8, window
=bpy
.context
.window
)
146 def demo_mode_timer_remove():
147 if global_state
["timer"]:
148 bpy
.context
.window_manager
.event_timer_remove(global_state
["timer"])
149 global_state
["timer"] = None
152 def demo_mode_load_file():
153 """ Take care, this can only do limited functions since its running
154 before the file is fully loaded.
155 Some operators will crash like playing an animation.
157 print("func:demo_mode_load_file")
158 DemoMode
.first_run
= True
159 bpy
.ops
.wm
.demo_mode('EXEC_DEFAULT')
162 def demo_mode_temp_file():
163 """ Initialize a temp config for the duration of the play time.
164 Use this so we can initialize the demo intro screen but not show again.
166 assert(global_state
["demo_index"] == 0)
168 temp_config
= global_config_fallback
.copy()
169 temp_config
["anim_time_min"] = 0.0
170 temp_config
["anim_time_max"] = 60.0
171 temp_config
["anim_cycles"] = 0 # ensures we switch when hitting the end
172 temp_config
["mode"] = 'PLAY'
173 temp_config
["is_tmp"] = True
175 global_config_files
.insert(0, temp_config
)
178 def demo_mode_init():
179 print("func:demo_mode_init")
180 DemoKeepAlive
.ensure()
183 global_config
.clear()
184 global_config
.update(global_config_files
[global_state
["demo_index"]])
188 demo_mode_timer_add()
190 if global_config
["mode"] == 'AUTO':
191 global_config
["mode"] = demo_mode_auto_select()
193 if global_config
["mode"] == 'PLAY':
194 global_state
["last_frame"] = -1
195 global_state
["anim_cycles"] = 0
196 bpy
.ops
.screen
.animation_play()
198 elif global_config
["mode"] == 'RENDER':
202 scene
= bpy
.context
.scene
203 scene
.render
.filepath
= "TEMP_RENDER"
204 scene
.render
.image_settings
.file_format
= 'AVI_JPEG' if global_config
["anim_render"] else 'PNG'
205 scene
.render
.use_file_extension
= False
206 scene
.render
.use_placeholder
= False
208 # XXX - without this rendering will crash because of a bug in blender!
209 bpy
.ops
.wm
.redraw_timer(type='DRAW_WIN_SWAP', iterations
=1)
210 if global_config
["anim_render"]:
211 bpy
.ops
.render
.render('INVOKE_DEFAULT', animation
=True)
213 bpy
.ops
.render
.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
217 except RuntimeError: # no camera for eg:
219 traceback
.print_exc()
222 raise Exception("Unsupported mode %r" % global_config
["mode"])
224 global_state
["init_time"] = global_state
["last_switch"] = time
.time()
225 global_state
["render_time"] = -1.0
228 def demo_mode_update():
229 time_current
= time
.time()
230 time_delta
= time_current
- global_state
["last_switch"]
231 time_total
= time_current
- global_state
["init_time"]
233 # --------------------------------------------------------------------------
235 if global_config
["mode"] == 'PLAY':
236 frame
= bpy
.context
.scene
.frame_current
238 if time_total
> global_config
["anim_time_max"]:
239 demo_mode_next_file()
241 # above cycles and minimum display time
243 (time_total
> global_config
["anim_time_min"]) and
244 (global_state
["anim_cycles"] > global_config
["anim_cycles"])
247 demo_mode_next_file()
251 if global_state
["reset_anim"]:
252 global_state
["reset_anim"] = False
253 bpy
.ops
.screen
.animation_cancel(restore_frame
=False)
254 bpy
.ops
.screen
.animation_play()
256 # warning, switching the screen can switch the scene
257 # and mess with our last-frame/cycles counting.
258 if global_config
["anim_screen_switch"]:
259 # print(time_delta, 1)
260 if time_delta
> global_config
["anim_screen_switch"]:
261 window
= bpy
.context
.window
263 workspace
= window
.workspace
264 index
= bpy
.data
.workspaces
.keys().index(workspace
.name
)
265 workspace_new
= bpy
.data
.workspaces
[(index
+ 1) % len(bpy
.data
.workspaces
)]
266 window
.workspace
= workspace_new
268 global_state
["last_switch"] = time_current
270 # If we also switch scenes then reset last frame
271 # otherwise it could mess up cycle calculation.
272 if scene
!= window
.scene
:
273 global_state
["last_frame"] = -1
275 #if global_config["mode"] == 'PLAY':
277 global_state
["reset_anim"] = True
280 if global_state
["last_frame"] > frame
:
282 global_state
["anim_cycles"] += 1
284 global_state
["last_frame"] = frame
286 # --------------------------------------------------------------------------
288 elif global_config
["mode"] == 'RENDER':
289 if global_state
["is_render"]:
290 # wait until the time has passed
291 # XXX, todo, if rendering an anim we need some way to check its done.
292 if global_state
["render_time"] == -1.0:
293 global_state
["render_time"] = time
.time()
295 if time
.time() - global_state
["render_time"] > global_config
["display_render"]:
296 handle_render_clear()
297 demo_mode_next_file()
300 raise Exception("Unsupported mode %r" % global_config
["mode"])
302 # -----------------------------------------------------------------------------
307 secret_attr
= "_keepalive"
311 if DemoKeepAlive
.secret_attr
not in bpy
.app
.driver_namespace
:
312 bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
] = DemoKeepAlive()
316 if DemoKeepAlive
.secret_attr
in bpy
.app
.driver_namespace
:
317 del bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
]
320 """ Hack, when the file is loaded the drivers namespace is cleared.
323 demo_mode_load_file()
326 class DemoMode(bpy
.types
.Operator
):
327 bl_idname
= "wm.demo_mode"
333 def cleanup(self
, disable
=False):
334 demo_mode_timer_remove()
335 DemoMode
.first_run
= True
338 DemoMode
.enabled
= False
339 DemoKeepAlive
.remove()
341 def modal(self
, context
, event
):
342 # print("DemoMode.modal", global_state["anim_cycles"])
343 if not DemoMode
.enabled
:
344 self
.cleanup(disable
=True)
347 if event
.type == 'ESC':
348 self
.cleanup(disable
=True)
349 # disable here and not in cleanup because this is a user level disable.
350 # which should stay disabled until explicitly enabled again.
354 if DemoMode
.first_run
:
355 DemoMode
.first_run
= False
361 return {'PASS_THROUGH'}
363 def execute(self
, context
):
364 print("func:DemoMode.execute:", len(global_config_files
), "files")
368 # load config if not loaded
369 if not global_config_files
:
373 if not global_config_files
:
374 self
.report({'INFO'}, "No configuration found with text or file: %s. Run File -> Demo Mode Setup" % DEMO_CFG
)
378 demo_mode_temp_file() # play this once through then never again
381 if DemoMode
.enabled
and DemoMode
.first_run
is False:
382 # this actually cancells the previous running instance
383 # should never happen now, DemoModeControl is for this.
386 DemoMode
.enabled
= True
388 context
.window_manager
.modal_handler_add(self
)
389 return {'RUNNING_MODAL'}
391 def cancel(self
, context
):
392 print("func:DemoMode.cancel")
393 # disable here means no running on file-load.
396 # call from DemoModeControl
399 if cls
.enabled
and cls
.first_run
is False:
400 # this actually cancells the previous running instance
401 # should never happen now, DemoModeControl is for this.
405 class DemoModeControl(bpy
.types
.Operator
):
406 bl_idname
= "wm.demo_mode_control"
409 mode
: bpy
.props
.EnumProperty(
410 items
=(('PREV', "Prev", ""),
411 ('PAUSE', "Pause", ""),
412 ('NEXT', "Next", "")),
416 def execute(self
, context
):
419 demo_mode_next_file(-1)
421 demo_mode_next_file(1)
427 def menu_func(self
, context
):
428 # 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"])
430 layout
.operator_context
= 'EXEC_DEFAULT'
431 row
= layout
.row(align
=True)
432 row
.label(text
="Demo Mode:")
433 if not DemoMode
.enabled
:
434 row
.operator("wm.demo_mode", icon
='PLAY', text
="")
436 row
.operator("wm.demo_mode_control", icon
='REW', text
="").mode
= 'PREV'
437 row
.operator("wm.demo_mode_control", icon
='PAUSE', text
="").mode
= 'PAUSE'
438 row
.operator("wm.demo_mode_control", icon
='FF', text
="").mode
= 'NEXT'
442 bpy
.utils
.register_class(DemoMode
)
443 bpy
.utils
.register_class(DemoModeControl
)
444 bpy
.types
.INFO_HT_header
.append(menu_func
)
448 bpy
.utils
.unregister_class(DemoMode
)
449 bpy
.utils
.unregister_class(DemoModeControl
)
450 bpy
.types
.INFO_HT_header
.remove(menu_func
)
453 # -----------------------------------------------------------------------------
456 def load_config(cfg_name
=DEMO_CFG
):
458 del global_config_files
[:]
459 basedir
= os
.path
.dirname(bpy
.data
.filepath
)
461 text
= bpy
.data
.texts
.get(cfg_name
)
463 demo_path
= os
.path
.join(basedir
, cfg_name
)
464 if os
.path
.exists(demo_path
):
465 print("Using config file: %r" % demo_path
)
466 demo_file
= open(demo_path
, "r")
467 demo_data
= demo_file
.read()
472 print("Using config textblock: %r" % cfg_name
)
473 demo_data
= text
.as_string()
474 demo_path
= os
.path
.join(bpy
.data
.filepath
, cfg_name
) # fake
477 print("Could not find %r textblock or %r file." % (DEMO_CFG
, demo_path
))
480 namespace
["__file__"] = demo_path
482 exec(demo_data
, namespace
, namespace
)
484 demo_config
= namespace
["config"]
485 demo_search_path
= namespace
.get("search_path")
486 global_state
["exit"] = namespace
.get("exit", False)
488 if demo_search_path
is None:
489 print("reading: %r, no search_path found, missing files wont be searched." % demo_path
)
490 if demo_search_path
.startswith("//"):
491 demo_search_path
= bpy
.path
.abspath(demo_search_path
)
492 if not os
.path
.exists(demo_search_path
):
493 print("reading: %r, search_path %r does not exist." % (demo_path
, demo_search_path
))
494 demo_search_path
= None
497 # initialize once, case insensitive dict
499 def lookup_file(filepath
):
500 filename
= os
.path
.basename(filepath
).lower()
503 # ensure only ever run once.
504 blend_lookup
[None] = None
506 def blend_dict_items(path
):
507 for dirpath
, dirnames
, filenames
in os
.walk(path
):
509 dirnames
[:] = [d
for d
in dirnames
if not d
.startswith(".")]
510 for filename
in filenames
:
511 if filename
.lower().endswith(".blend"):
512 filepath
= os
.path
.join(dirpath
, filename
)
513 yield (filename
.lower(), filepath
)
515 blend_lookup
.update(dict(blend_dict_items(demo_search_path
)))
517 # fallback to original file
518 return blend_lookup
.get(filename
, filepath
)
519 # done with search lookup
521 for filecfg
in demo_config
:
522 filepath_test
= filecfg
["file"]
523 if not os
.path
.exists(filepath_test
):
524 filepath_test
= os
.path
.join(basedir
, filecfg
["file"])
525 if not os
.path
.exists(filepath_test
):
526 filepath_test
= lookup_file(filepath_test
) # attempt to get from searchpath
527 if not os
.path
.exists(filepath_test
):
528 print("Cant find %r or %r, skipping!")
531 filecfg
["file"] = os
.path
.normpath(filepath_test
)
534 filecfg
["file"] = os
.path
.abspath(filecfg
["file"])
535 filecfg
["file"] = os
.path
.normpath(filecfg
["file"])
536 print(" Adding: %r" % filecfg
["file"])
537 global_config_files
.append(filecfg
)
539 print("found %d files" % len(global_config_files
))
541 global_state
["basedir"] = basedir
543 return bool(global_config_files
)
546 # support direct execution
547 if __name__
== "__main__":
550 demo_mode_load_file() # kick starts the modal operator