1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
22 Even though this is in a package this can run as a stand alone scripts.
25 blender --python release/scripts/addons/system_demo_mode/demo_mode.py
27 looks for demo.py textblock or file in the same path as the blend:
30 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'),
31 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'),
34 /data/src/blender/lib/tests/rendering/
43 # populate from script
44 global_config_files
= []
49 anim_screen_switch
=0.0,
56 # switch to the next file in 2 sec.
57 global_config_fallback
= dict(
60 anim_screen_switch
=0.0,
71 "anim_cycles": 0, # count how many times we played the anim
74 "render_time": "", # time render was finished.
76 "basedir": "", # demo.py is stored here
82 # -----------------------------------------------------------------------------
83 # render handler - maintain "is_render"
85 def handle_render_clear():
86 for ls
in (bpy
.app
.handlers
.render_complete
, bpy
.app
.handlers
.render_cancel
):
87 while handle_render_done_cb
in ls
:
88 ls
.remove(handle_render_done_cb
)
91 def handle_render_done_cb(self
):
92 global_state
["is_render"] = True
95 def handle_render_init():
97 bpy
.app
.handlers
.render_complete
.append(handle_render_done_cb
)
98 bpy
.app
.handlers
.render_cancel
.append(handle_render_done_cb
)
99 global_state
["is_render"] = False
102 def demo_mode_auto_select():
109 for area
in bpy
.context
.window
.screen
.areas
:
110 size
= area
.width
* area
.height
111 if area
.type in {'VIEW_3D', 'GRAPH_EDITOR', 'DOPESHEET_EDITOR', 'NLA_EDITOR', 'TIMELINE'}:
113 elif area
.type in {'IMAGE_EDITOR', 'SEQUENCE_EDITOR', 'NODE_EDITOR'}:
116 if area
.type == 'IMAGE_EDITOR':
119 # since our test files have this as defacto standard
120 scene
= bpy
.context
.scene
121 if totimg
>= 2 and (scene
.camera
or scene
.render
.use_sequencer
):
124 if play_area
>= render_area
:
135 def demo_mode_next_file(step
=1):
138 if global_config_files
[global_state
["demo_index"]].get("is_tmp"):
139 del global_config_files
[global_state
["demo_index"]]
140 global_state
["demo_index"] -= 1
142 print(global_state
["demo_index"])
143 demo_index_next
= (global_state
["demo_index"] + step
) % len(global_config_files
)
145 if global_state
["exit"] and step
> 0:
147 if demo_index_next
< global_state
["demo_index"]:
151 global_state
["demo_index"] = demo_index_next
152 print(global_state
["demo_index"], "....")
153 print("func:demo_mode_next_file", global_state
["demo_index"])
154 filepath
= global_config_files
[global_state
["demo_index"]]["file"]
155 bpy
.ops
.wm
.open_mainfile(filepath
=filepath
)
158 def demo_mode_timer_add():
159 global_state
["timer"] = bpy
.context
.window_manager
.event_timer_add(0.8, window
=bpy
.context
.window
)
162 def demo_mode_timer_remove():
163 if global_state
["timer"]:
164 bpy
.context
.window_manager
.event_timer_remove(global_state
["timer"])
165 global_state
["timer"] = None
168 def demo_mode_load_file():
169 """ Take care, this can only do limited functions since its running
170 before the file is fully loaded.
171 Some operators will crash like playing an animation.
173 print("func:demo_mode_load_file")
174 DemoMode
.first_run
= True
175 bpy
.ops
.wm
.demo_mode('EXEC_DEFAULT')
178 def demo_mode_temp_file():
179 """ Initialize a temp config for the duration of the play time.
180 Use this so we can initialize the demo intro screen but not show again.
182 assert(global_state
["demo_index"] == 0)
184 temp_config
= global_config_fallback
.copy()
185 temp_config
["anim_time_min"] = 0.0
186 temp_config
["anim_time_max"] = 60.0
187 temp_config
["anim_cycles"] = 0 # ensures we switch when hitting the end
188 temp_config
["mode"] = 'PLAY'
189 temp_config
["is_tmp"] = True
191 global_config_files
.insert(0, temp_config
)
194 def demo_mode_init():
195 print("func:demo_mode_init")
196 DemoKeepAlive
.ensure()
199 global_config
.clear()
200 global_config
.update(global_config_files
[global_state
["demo_index"]])
204 demo_mode_timer_add()
206 if global_config
["mode"] == 'AUTO':
207 global_config
["mode"] = demo_mode_auto_select()
209 if global_config
["mode"] == 'PLAY':
210 global_state
["last_frame"] = -1
211 global_state
["anim_cycles"] = 0
212 bpy
.ops
.screen
.animation_play()
214 elif global_config
["mode"] == 'RENDER':
218 scene
= bpy
.context
.scene
219 scene
.render
.filepath
= "TEMP_RENDER"
220 scene
.render
.image_settings
.file_format
= 'AVI_JPEG' if global_config
["anim_render"] else 'PNG'
221 scene
.render
.use_file_extension
= False
222 scene
.render
.use_placeholder
= False
224 # XXX - without this rendering will crash because of a bug in blender!
225 bpy
.ops
.wm
.redraw_timer(type='DRAW_WIN_SWAP', iterations
=1)
226 if global_config
["anim_render"]:
227 bpy
.ops
.render
.render('INVOKE_DEFAULT', animation
=True)
229 bpy
.ops
.render
.render('INVOKE_DEFAULT') # write_still=True, no need to write now.
233 except RuntimeError: # no camera for eg:
235 traceback
.print_exc()
238 raise Exception("Unsupported mode %r" % global_config
["mode"])
240 global_state
["init_time"] = global_state
["last_switch"] = time
.time()
241 global_state
["render_time"] = -1.0
244 def demo_mode_update():
245 time_current
= time
.time()
246 time_delta
= time_current
- global_state
["last_switch"]
247 time_total
= time_current
- global_state
["init_time"]
249 # --------------------------------------------------------------------------
251 if global_config
["mode"] == 'PLAY':
252 frame
= bpy
.context
.scene
.frame_current
254 if time_total
> global_config
["anim_time_max"]:
255 demo_mode_next_file()
257 # above cycles and minimum display time
259 (time_total
> global_config
["anim_time_min"]) and
260 (global_state
["anim_cycles"] > global_config
["anim_cycles"])
263 demo_mode_next_file()
267 if global_state
["reset_anim"]:
268 global_state
["reset_anim"] = False
269 bpy
.ops
.screen
.animation_cancel(restore_frame
=False)
270 bpy
.ops
.screen
.animation_play()
272 # warning, switching the screen can switch the scene
273 # and mess with our last-frame/cycles counting.
274 if global_config
["anim_screen_switch"]:
275 # print(time_delta, 1)
276 if time_delta
> global_config
["anim_screen_switch"]:
278 screen
= bpy
.context
.window
.screen
279 index
= bpy
.data
.screens
.keys().index(screen
.name
)
280 screen_new
= bpy
.data
.screens
[(index
if index
> 0 else len(bpy
.data
.screens
)) - 1]
281 bpy
.context
.window
.screen
= screen_new
283 global_state
["last_switch"] = time_current
285 # if we also switch scenes then reset last frame
286 # otherwise it could mess up cycle calc.
287 if screen
.scene
!= screen_new
.scene
:
288 global_state
["last_frame"] = -1
290 #if global_config["mode"] == 'PLAY':
292 global_state
["reset_anim"] = True
295 if global_state
["last_frame"] > frame
:
297 global_state
["anim_cycles"] += 1
299 global_state
["last_frame"] = frame
301 # --------------------------------------------------------------------------
303 elif global_config
["mode"] == 'RENDER':
304 if global_state
["is_render"]:
305 # wait until the time has passed
306 # XXX, todo, if rendering an anim we need some way to check its done.
307 if global_state
["render_time"] == -1.0:
308 global_state
["render_time"] = time
.time()
310 if time
.time() - global_state
["render_time"] > global_config
["display_render"]:
311 handle_render_clear()
312 demo_mode_next_file()
315 raise Exception("Unsupported mode %r" % global_config
["mode"])
317 # -----------------------------------------------------------------------------
322 secret_attr
= "_keepalive"
326 if DemoKeepAlive
.secret_attr
not in bpy
.app
.driver_namespace
:
327 bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
] = DemoKeepAlive()
331 if DemoKeepAlive
.secret_attr
in bpy
.app
.driver_namespace
:
332 del bpy
.app
.driver_namespace
[DemoKeepAlive
.secret_attr
]
335 """ Hack, when the file is loaded the drivers namespace is cleared.
338 demo_mode_load_file()
341 class DemoMode(bpy
.types
.Operator
):
342 bl_idname
= "wm.demo_mode"
348 def cleanup(self
, disable
=False):
349 demo_mode_timer_remove()
350 DemoMode
.first_run
= True
353 DemoMode
.enabled
= False
354 DemoKeepAlive
.remove()
356 def modal(self
, context
, event
):
357 # print("DemoMode.modal", global_state["anim_cycles"])
358 if not DemoMode
.enabled
:
359 self
.cleanup(disable
=True)
362 if event
.type == 'ESC':
363 self
.cleanup(disable
=True)
364 # disable here and not in cleanup because this is a user level disable.
365 # which should stay disabled until explicitly enabled again.
369 if DemoMode
.first_run
:
370 DemoMode
.first_run
= False
376 return {'PASS_THROUGH'}
378 def execute(self
, context
):
379 print("func:DemoMode.execute:", len(global_config_files
), "files")
383 # load config if not loaded
384 if not global_config_files
:
388 if not global_config_files
:
389 self
.report({'INFO'}, "No configuration found with text or file: %s. Run File -> Demo Mode Setup" % DEMO_CFG
)
393 demo_mode_temp_file() # play this once through then never again
396 if DemoMode
.enabled
and DemoMode
.first_run
is False:
397 # this actually cancells the previous running instance
398 # should never happen now, DemoModeControl is for this.
401 DemoMode
.enabled
= True
403 context
.window_manager
.modal_handler_add(self
)
404 return {'RUNNING_MODAL'}
406 def cancel(self
, context
):
407 print("func:DemoMode.cancel")
408 # disable here means no running on file-load.
411 # call from DemoModeControl
414 if cls
.enabled
and cls
.first_run
is False:
415 # this actually cancells the previous running instance
416 # should never happen now, DemoModeControl is for this.
420 class DemoModeControl(bpy
.types
.Operator
):
421 bl_idname
= "wm.demo_mode_control"
424 mode
: bpy
.props
.EnumProperty(
425 items
=(('PREV', "Prev", ""),
426 ('PAUSE', "Pause", ""),
427 ('NEXT', "Next", "")),
431 def execute(self
, context
):
434 demo_mode_next_file(-1)
436 demo_mode_next_file(1)
442 def menu_func(self
, context
):
443 # 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"])
445 layout
.operator_context
= 'EXEC_DEFAULT'
446 row
= layout
.row(align
=True)
447 row
.label(text
="Demo Mode:")
448 if not DemoMode
.enabled
:
449 row
.operator("wm.demo_mode", icon
='PLAY', text
="")
451 row
.operator("wm.demo_mode_control", icon
='REW', text
="").mode
= 'PREV'
452 row
.operator("wm.demo_mode_control", icon
='PAUSE', text
="").mode
= 'PAUSE'
453 row
.operator("wm.demo_mode_control", icon
='FF', text
="").mode
= 'NEXT'
457 bpy
.utils
.register_class(DemoMode
)
458 bpy
.utils
.register_class(DemoModeControl
)
459 bpy
.types
.INFO_HT_header
.append(menu_func
)
463 bpy
.utils
.unregister_class(DemoMode
)
464 bpy
.utils
.unregister_class(DemoModeControl
)
465 bpy
.types
.INFO_HT_header
.remove(menu_func
)
468 # -----------------------------------------------------------------------------
471 def load_config(cfg_name
=DEMO_CFG
):
473 del global_config_files
[:]
474 basedir
= os
.path
.dirname(bpy
.data
.filepath
)
476 text
= bpy
.data
.texts
.get(cfg_name
)
478 demo_path
= os
.path
.join(basedir
, cfg_name
)
479 if os
.path
.exists(demo_path
):
480 print("Using config file: %r" % demo_path
)
481 demo_file
= open(demo_path
, "r")
482 demo_data
= demo_file
.read()
487 print("Using config textblock: %r" % cfg_name
)
488 demo_data
= text
.as_string()
489 demo_path
= os
.path
.join(bpy
.data
.filepath
, cfg_name
) # fake
492 print("Could not find %r textblock or %r file." % (DEMO_CFG
, demo_path
))
495 namespace
["__file__"] = demo_path
497 exec(demo_data
, namespace
, namespace
)
499 demo_config
= namespace
["config"]
500 demo_search_path
= namespace
.get("search_path")
501 global_state
["exit"] = namespace
.get("exit", False)
503 if demo_search_path
is None:
504 print("reading: %r, no search_path found, missing files wont be searched." % demo_path
)
505 if demo_search_path
.startswith("//"):
506 demo_search_path
= bpy
.path
.abspath(demo_search_path
)
507 if not os
.path
.exists(demo_search_path
):
508 print("reading: %r, search_path %r does not exist." % (demo_path
, demo_search_path
))
509 demo_search_path
= None
512 # initialize once, case insensitive dict
514 def lookup_file(filepath
):
515 filename
= os
.path
.basename(filepath
).lower()
518 # ensure only ever run once.
519 blend_lookup
[None] = None
521 def blend_dict_items(path
):
522 for dirpath
, dirnames
, filenames
in os
.walk(path
):
524 dirnames
[:] = [d
for d
in dirnames
if not d
.startswith(".")]
525 for filename
in filenames
:
526 if filename
.lower().endswith(".blend"):
527 filepath
= os
.path
.join(dirpath
, filename
)
528 yield (filename
.lower(), filepath
)
530 blend_lookup
.update(dict(blend_dict_items(demo_search_path
)))
532 # fallback to original file
533 return blend_lookup
.get(filename
, filepath
)
534 # done with search lookup
536 for filecfg
in demo_config
:
537 filepath_test
= filecfg
["file"]
538 if not os
.path
.exists(filepath_test
):
539 filepath_test
= os
.path
.join(basedir
, filecfg
["file"])
540 if not os
.path
.exists(filepath_test
):
541 filepath_test
= lookup_file(filepath_test
) # attempt to get from searchpath
542 if not os
.path
.exists(filepath_test
):
543 print("Cant find %r or %r, skipping!")
546 filecfg
["file"] = os
.path
.normpath(filepath_test
)
549 filecfg
["file"] = os
.path
.abspath(filecfg
["file"])
550 filecfg
["file"] = os
.path
.normpath(filecfg
["file"])
551 print(" Adding: %r" % filecfg
["file"])
552 global_config_files
.append(filecfg
)
554 print("found %d files" % len(global_config_files
))
556 global_state
["basedir"] = basedir
558 return bool(global_config_files
)
561 # support direct execution
562 if __name__
== "__main__":
565 demo_mode_load_file() # kick starts the modal operator