1 # SPDX-License-Identifier: GPL-2.0-or-later
4 Even though this is in a package this can run as a stand alone scripts.
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:
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'),
16 /data/src/blender/lib/tests/rendering/
25 # populate from script
26 global_config_files
= []
31 anim_screen_switch
=0.0,
38 # switch to the next file in 2 sec.
39 global_config_fallback
= dict(
42 anim_screen_switch
=0.0,
53 "anim_cycles": 0, # count how many times we played the anim
56 "render_time": "", # time render was finished.
58 "basedir": "", # demo.py is stored here
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():
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():
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'}:
95 elif area
.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
98 if area
.type == 'IMAGE_EDITOR':
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
):
106 if play_area
>= render_area
:
117 def demo_mode_next_file(step
=1):
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:
129 if demo_index_next
< global_state
["demo_index"]:
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()
181 global_config
.clear()
182 global_config
.update(global_config_files
[global_state
["demo_index"]])
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':
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
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)
211 bpy
.ops
.render
.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
215 except RuntimeError: # no camera for eg:
217 traceback
.print_exc()
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 # --------------------------------------------------------------------------
233 if global_config
["mode"] == 'PLAY':
234 frame
= bpy
.context
.scene
.frame_current
236 if time_total
> global_config
["anim_time_max"]:
237 demo_mode_next_file()
239 # above cycles and minimum display time
241 (time_total
> global_config
["anim_time_min"]) and
242 (global_state
["anim_cycles"] > global_config
["anim_cycles"])
245 demo_mode_next_file()
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
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':
275 global_state
["reset_anim"] = True
278 if global_state
["last_frame"] > frame
:
280 global_state
["anim_cycles"] += 1
282 global_state
["last_frame"] = frame
284 # --------------------------------------------------------------------------
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()
293 if time
.time() - global_state
["render_time"] > global_config
["display_render"]:
294 handle_render_clear()
295 demo_mode_next_file()
298 raise Exception("Unsupported mode %r" % global_config
["mode"])
300 # -----------------------------------------------------------------------------
305 secret_attr
= "_keepalive"
309 if DemoKeepAlive
.secret_attr
not in bpy
.app
.driver_namespace
:
310 bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
] = DemoKeepAlive()
314 if DemoKeepAlive
.secret_attr
in bpy
.app
.driver_namespace
:
315 del bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
]
318 """ Hack, when the file is loaded the drivers namespace is cleared.
321 demo_mode_load_file()
324 class DemoMode(bpy
.types
.Operator
):
325 bl_idname
= "wm.demo_mode"
331 def cleanup(self
, disable
=False):
332 demo_mode_timer_remove()
333 DemoMode
.first_run
= True
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)
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.
352 if DemoMode
.first_run
:
353 DemoMode
.first_run
= False
359 return {'PASS_THROUGH'}
361 def execute(self
, context
):
362 print("func:DemoMode.execute:", len(global_config_files
), "files")
366 # load config if not loaded
367 if not global_config_files
:
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
)
376 demo_mode_temp_file() # play this once through then never again
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.
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.
394 # call from DemoModeControl
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.
403 class DemoModeControl(bpy
.types
.Operator
):
404 bl_idname
= "wm.demo_mode_control"
407 mode
: bpy
.props
.EnumProperty(
408 items
=(('PREV', "Prev", ""),
409 ('PAUSE', "Pause", ""),
410 ('NEXT', "Next", "")),
414 def execute(self
, context
):
417 demo_mode_next_file(-1)
419 demo_mode_next_file(1)
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"])
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
="")
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'
440 bpy
.utils
.register_class(DemoMode
)
441 bpy
.utils
.register_class(DemoModeControl
)
442 bpy
.types
.INFO_HT_header
.append(menu_func
)
446 bpy
.utils
.unregister_class(DemoMode
)
447 bpy
.utils
.unregister_class(DemoModeControl
)
448 bpy
.types
.INFO_HT_header
.remove(menu_func
)
451 # -----------------------------------------------------------------------------
454 def load_config(cfg_name
=DEMO_CFG
):
456 del global_config_files
[:]
457 basedir
= os
.path
.dirname(bpy
.data
.filepath
)
459 text
= bpy
.data
.texts
.get(cfg_name
)
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()
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
475 print("Could not find %r textblock or %r file." % (DEMO_CFG
, demo_path
))
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
495 # initialize once, case insensitive dict
497 def lookup_file(filepath
):
498 filename
= os
.path
.basename(filepath
).lower()
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
):
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!")
529 filecfg
["file"] = os
.path
.normpath(filepath_test
)
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__":
548 demo_mode_load_file() # kick starts the modal operator