Merged changes in the trunk up to revision 44266 (including BMesh).
[blender.git] / source / tests / bl_mesh_modifiers.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
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.
7 #
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.
12 #
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.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 # Currently this script only generates images from different modifier
22 # combinations and does not validate they work correctly,
23 # this is because we dont get 1:1 match with bmesh.
24 #
25 # Later, we may have a way to check the results are valid.
26
27
28 # ./blender.bin --factory-startup --python source/tests/bl_mesh_modifiers.py
29 #
30
31 import math
32
33 USE_QUICK_RENDER = False
34 IS_BMESH = hasattr(__import__("bpy").types, "LoopColors")
35
36 # -----------------------------------------------------------------------------
37 # utility funcs
38
39
40 def render_gl(context, filepath, shade):
41
42     def ctx_viewport_shade(context, shade):
43         for area in context.window.screen.areas:
44             if area.type == 'VIEW_3D':
45                 space = area.spaces.active
46                 # rv3d = space.region_3d
47                 space.viewport_shade = shade
48
49     import bpy
50     scene = context.scene
51     render = scene.render
52     render.filepath = filepath
53     render.image_settings.file_format = 'PNG'
54     render.image_settings.color_mode = 'RGB'
55     render.use_file_extension = True
56     render.use_antialiasing = False
57
58     # render size
59     render.resolution_percentage = 100
60     render.resolution_x = 512
61     render.resolution_y = 512
62
63     ctx_viewport_shade(context, shade)
64
65     #~ # stop to inspect!
66     #~ if filepath == "test_cube_shell_solidify_subsurf_wp_wire":
67         #~ assert(0)
68     #~ else:
69         #~ return
70
71     bpy.ops.render.opengl(write_still=True,
72                           view_context=True)
73
74
75 def render_gl_all_modes(context, obj, filepath=""):
76
77     assert(obj is not None)
78     assert(filepath != "")
79
80     scene = context.scene
81
82     # avoid drawing outline/center dot
83     bpy.ops.object.select_all(action='DESELECT')
84     scene.objects.active = None
85
86     # editmode
87     scene.tool_settings.mesh_select_mode = False, True, False
88
89     # render
90     render_gl(context, filepath + "_ob_solid", shade='SOLID')
91
92     if USE_QUICK_RENDER:
93         return
94
95     render_gl(context, filepath + "_ob_wire", shade='WIREFRAME')
96     render_gl(context, filepath + "_ob_textured", shade='TEXTURED')
97
98     # -------------------------------------------------------------------------
99     # not just draw modes, but object modes!
100     scene.objects.active = obj
101
102     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
103     bpy.ops.mesh.select_all(action='DESELECT')
104     render_gl(context, filepath + "_edit_wire", shade='WIREFRAME')
105     render_gl(context, filepath + "_edit_solid", shade='SOLID')
106     render_gl(context, filepath + "_edit_textured", shade='TEXTURED')
107     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
108
109     bpy.ops.object.mode_set(mode='WEIGHT_PAINT', toggle=False)
110
111     render_gl(context, filepath + "_wp_wire", shade='WIREFRAME')
112
113     assert(1)
114
115     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
116
117     scene.objects.active = None
118
119
120 def ctx_clear_scene():  # copied from batch_import.py
121     import bpy
122     unique_obs = set()
123     for scene in bpy.data.scenes:
124         for obj in scene.objects[:]:
125             scene.objects.unlink(obj)
126             unique_obs.add(obj)
127
128     # remove obdata, for now only worry about the startup scene
129     for bpy_data_iter in (bpy.data.objects,
130                           bpy.data.meshes,
131                           bpy.data.lamps,
132                           bpy.data.cameras,
133                           ):
134
135         for id_data in bpy_data_iter:
136             bpy_data_iter.remove(id_data)
137
138
139 def ctx_viewport_camera(context):
140     # because gl render without view_context has no shading option.
141     for area in context.window.screen.areas:
142         if area.type == 'VIEW_3D':
143             space = area.spaces.active
144             space.region_3d.view_perspective = 'CAMERA'
145
146
147 def ctx_camera_setup(context,
148                      location=(0.0, 0.0, 0.0),
149                      lookat=(0.0, 0.0, 0.0),
150                      # most likely the followuing vars can be left as defaults
151                      up=(0.0, 0.0, 1.0),
152                      lookat_axis='-Z',
153                      up_axis='Y',
154                      ):
155
156     camera = bpy.data.cameras.new(whoami())
157     obj = bpy.data.objects.new(whoami(), camera)
158
159     scene = context.scene
160     scene.objects.link(obj)
161     scene.camera = obj
162
163     from mathutils import Vector, Matrix
164
165     # setup transform
166     view_vec = Vector(lookat) - Vector(location)
167     rot_mat = view_vec.to_track_quat(lookat_axis, up_axis).to_matrix().to_4x4()
168     tra_mat = Matrix.Translation(location)
169
170     obj.matrix_world = tra_mat * rot_mat
171
172     ctx_viewport_camera(context)
173
174     return obj
175
176
177 # -----------------------------------------------------------------------------
178 # inspect functions
179
180 import inspect
181
182
183 # functions
184
185 def whoami():
186     return inspect.stack()[1][3]
187
188
189 def whosdaddy():
190     return inspect.stack()[2][3]
191
192
193 # -----------------------------------------------------------------------------
194 # models (defaults)
195
196 def defaults_object(obj):
197     obj.show_wire = True
198
199     if obj.type == 'MESH':
200         mesh = obj.data
201         mesh.show_all_edges = True
202
203         mesh.show_normal_vertex = True
204
205         # lame!
206         if IS_BMESH:
207             for poly in mesh.polygons:
208                 poly.use_smooth = True
209         else:
210             for face in mesh.faces:
211                 face.use_smooth = True
212
213
214 def defaults_modifier(mod):
215     mod.show_in_editmode = True
216     mod.show_on_cage = True
217
218
219 # -----------------------------------------------------------------------------
220 # models (utils)
221
222
223 if IS_BMESH:
224     def mesh_bmesh_poly_elems(poly, elems):
225         vert_start = poly.loop_start
226         vert_total = poly.loop_total
227         return elems[vert_start:vert_start + vert_total]
228
229     def mesh_bmesh_poly_vertices(poly):
230         return [loop.vertex_index
231                 for loop in mesh_bmesh_poly_elems(poly, poly.id_data.loops)]
232
233
234 def mesh_bounds(mesh):
235     xmin = ymin = zmin = +100000000.0
236     xmax = ymax = zmax = -100000000.0
237
238     for v in mesh.vertices:
239         x, y, z = v.co
240         xmax = max(x, xmax)
241         ymax = max(y, ymax)
242         zmax = max(z, zmax)
243
244         xmin = min(x, xmin)
245         ymin = min(y, ymin)
246         zmin = min(z, zmin)
247
248     return (xmin, ymin, zmin), (xmax, ymax, zmax)
249
250
251 def mesh_uv_add(obj):
252
253     uvs = ((0.0, 0.0),
254            (0.0, 1.0),
255            (1.0, 1.0),
256            (1.0, 0.0))
257
258     uv_lay = obj.data.uv_textures.new()
259
260     if IS_BMESH:
261         # XXX, odd that we need to do this. until uvs and texface
262         # are separated we will need to keep it
263         uv_loops = obj.data.uv_loop_layers[-1]
264         uv_list = uv_loops.data[:]
265         for poly in obj.data.polygons:
266             poly_uvs = mesh_bmesh_poly_elems(poly, uv_list)
267             for i, c in enumerate(poly_uvs):
268                 c.uv = uvs[i % 4]
269     else:
270         for uv in uv_lay.data:
271             uv.uv1 = uvs[0]
272             uv.uv2 = uvs[1]
273             uv.uv3 = uvs[2]
274             uv.uv4 = uvs[3]
275
276     return uv_lay
277
278
279 def mesh_vcol_add(obj, mode=0):
280
281     colors = ((0.0, 0.0, 0.0),  # black
282               (1.0, 0.0, 0.0),  # red
283               (0.0, 1.0, 0.0),  # green
284               (0.0, 0.0, 1.0),  # blue
285               (1.0, 1.0, 0.0),  # yellow
286               (0.0, 1.0, 1.0),  # cyan
287               (1.0, 0.0, 1.0),  # magenta
288               (1.0, 1.0, 1.0),  # white
289               )
290
291     def colors_get(i):
292         return colors[i % len(colors)]
293
294     vcol_lay = obj.data.vertex_colors.new()
295
296     mesh = obj.data
297
298     if IS_BMESH:
299         col_list = vcol_lay.data[:]
300         for poly in mesh.polygons:
301             face_verts = mesh_bmesh_poly_vertices(poly)
302             poly_cols = mesh_bmesh_poly_elems(poly, col_list)
303             for i, c in enumerate(poly_cols):
304                 c.color = colors_get(face_verts[i])
305     else:
306         for i, col in enumerate(vcol_lay.data):
307             face_verts = mesh.faces[i].vertices
308             col.color1 = colors_get(face_verts[0])
309             col.color2 = colors_get(face_verts[1])
310             col.color3 = colors_get(face_verts[2])
311             if len(face_verts) == 4:
312                 col.color4 = colors_get(face_verts[3])
313
314     return vcol_lay
315
316
317 def mesh_vgroup_add(obj, name="Group", axis=0, invert=False, mode=0):
318     mesh = obj.data
319     vgroup = obj.vertex_groups.new(name=name)
320     vgroup.add(list(range(len(mesh.vertices))), 1.0, 'REPLACE')
321     group_index = len(obj.vertex_groups) - 1
322
323     min_bb, max_bb = mesh_bounds(mesh)
324
325     range_axis = max_bb[axis] - min_bb[axis]
326
327     # gradient
328     for v in mesh.vertices:
329         for vg in v.groups:
330             if vg.group == group_index:
331                 f = (v.co[axis] - min_bb[axis]) / range_axis
332                 vg.weight = 1.0 - f if invert else f
333
334     return vgroup
335
336
337 def mesh_shape_add(obj, mode=0):
338     pass
339
340
341 def mesh_armature_add(obj, mode=0):
342     pass
343
344
345 # -----------------------------------------------------------------------------
346 # modifiers
347
348 def modifier_subsurf_add(scene, obj, levels=2):
349     mod = obj.modifiers.new(name=whoami(), type='SUBSURF')
350     defaults_modifier(mod)
351
352     mod.levels = levels
353     mod.render_levels = levels
354     return mod
355
356
357 def modifier_armature_add(scene, obj):
358     mod = obj.modifiers.new(name=whoami(), type='ARMATURE')
359     defaults_modifier(mod)
360
361     arm_data = bpy.data.armatures.new(whoami())
362     obj_arm = bpy.data.objects.new(whoami(), arm_data)
363
364     scene.objects.link(obj_arm)
365
366     obj_arm.select = True
367     scene.objects.active = obj_arm
368
369     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
370     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
371
372     # XXX, annoying, remove bone.
373     while arm_data.edit_bones:
374         obj_arm.edit_bones.remove(arm_data.edit_bones[-1])
375
376     bone_a = arm_data.edit_bones.new("Bone.A")
377     bone_b = arm_data.edit_bones.new("Bone.B")
378     bone_b.parent = bone_a
379
380     bone_a.head = -1, 0, 0
381     bone_a.tail = 0, 0, 0
382     bone_b.head = 0, 0, 0
383     bone_b.tail = 1, 0, 0
384
385     # Get armature animation data
386     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
387
388     # 45d armature
389     obj_arm.pose.bones["Bone.B"].rotation_quaternion = 1, -0.5, 0, 0
390
391     # set back to the original
392     scene.objects.active = obj
393
394     # display options
395     obj_arm.show_x_ray = True
396     arm_data.draw_type = 'STICK'
397
398     # apply to modifier
399     mod.object = obj_arm
400
401     mesh_vgroup_add(obj, name="Bone.A", axis=0, invert=True)
402     mesh_vgroup_add(obj, name="Bone.B", axis=0, invert=False)
403
404     return mod
405
406
407 def modifier_mirror_add(scene, obj):
408     mod = obj.modifiers.new(name=whoami(), type='MIRROR')
409     defaults_modifier(mod)
410
411     return mod
412
413
414 def modifier_solidify_add(scene, obj, thickness=0.25):
415     mod = obj.modifiers.new(name=whoami(), type='SOLIDIFY')
416     defaults_modifier(mod)
417
418     mod.thickness = thickness
419
420     return mod
421
422
423 def modifier_hook_add(scene, obj, use_vgroup=True):
424     scene.objects.active = obj
425
426     # no nice way to add hooks from py api yet
427     # assume object mode, hook first face!
428     mesh = obj.data
429
430     if use_vgroup:
431         for v in mesh.vertices:
432             v.select = True
433     else:
434         for v in mesh.vertices:
435             v.select = False
436
437         if IS_BMESH:
438             face_verts = mesh_bmesh_poly_vertices(mesh.polygons[0])
439         else:
440             face_verts = mesh.faces[0].vertices[:]
441
442         for i in mesh.faces[0].vertices:
443             mesh.vertices[i].select = True
444
445     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
446     bpy.ops.object.hook_add_newob()
447     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
448
449     # mod = obj.modifiers.new(name=whoami(), type='HOOK')
450     mod = obj.modifiers[-1]
451     defaults_modifier(mod)
452
453     obj_hook = mod.object
454     obj_hook.rotation_euler = 0, math.radians(45), 0
455     obj_hook.show_x_ray = True
456
457     if use_vgroup:
458         mod.vertex_group = obj.vertex_groups[0].name
459
460     return mod
461
462
463 def modifier_decimate_add(scene, obj):
464     mod = obj.modifiers.new(name=whoami(), type='DECIMATE')
465     defaults_modifier(mod)
466
467     mod.ratio = 1 / 3
468
469     return mod
470
471
472 def modifier_build_add(scene, obj):
473     mod = obj.modifiers.new(name=whoami(), type='BUILD')
474     defaults_modifier(mod)
475
476     # ensure we display some faces
477     if IS_BMESH:
478         totface = len(obj.data.polygons)
479     else:
480         totface = len(obj.data.faces)
481
482     mod.frame_start = totface // 2
483     mod.frame_duration = totface
484
485     return mod
486
487
488 def modifier_mask_add(scene, obj):
489     mod = obj.modifiers.new(name=whoami(), type='MASK')
490     defaults_modifier(mod)
491
492     mod.vertex_group = obj.vertex_groups[0].name
493
494     return mod
495
496
497 # -----------------------------------------------------------------------------
498 # models
499
500 # useful since its solid boxy shape but simple enough to debug errors
501 cube_like_vertices = (
502     (1, 1, -1),
503     (1, -1, -1),
504     (-1, -1, -1),
505     (-1, 1, -1),
506     (1, 1, 1),
507     (1, -1, 1),
508     (-1, -1, 1),
509     (-1, 1, 1),
510     (0, -1, -1),
511     (1, 0, -1),
512     (0, 1, -1),
513     (-1, 0, -1),
514     (1, 0, 1),
515     (0, -1, 1),
516     (-1, 0, 1),
517     (0, 1, 1),
518     (1, -1, 0),
519     (1, 1, 0),
520     (-1, -1, 0),
521     (-1, 1, 0),
522     (0, 0, -1),
523     (0, 0, 1),
524     (1, 0, 0),
525     (0, -1, 0),
526     (-1, 0, 0),
527     (2, 0, 0),
528     (2, 0, -1),
529     (2, 1, 0),
530     (2, 1, -1),
531     (0, 1, 2),
532     (0, 0, 2),
533     (-1, 0, 2),
534     (-1, 1, 2),
535     (-1, 0, 3),
536     (-1, 1, 3),
537     (0, 1, 3),
538     (0, 0, 3),
539     )
540
541
542 cube_like_faces = (
543     (0, 9, 20, 10),
544     (0, 10, 17),
545     (0, 17, 27, 28),
546     (1, 16, 23, 8),
547     (2, 18, 24, 11),
548     (3, 19, 10),
549     (4, 15, 21, 12),
550     (4, 17, 15),
551     (7, 14, 31, 32),
552     (7, 15, 19),
553     (8, 23, 18, 2),
554     (9, 0, 28, 26),
555     (9, 1, 8, 20),
556     (9, 22, 16, 1),
557     (10, 20, 11, 3),
558     (11, 24, 19, 3),
559     (12, 21, 13, 5),
560     (13, 6, 18),
561     (14, 21, 30, 31),
562     (15, 7, 32, 29),
563     (15, 17, 10, 19),
564     (16, 5, 13, 23),
565     (17, 4, 12, 22),
566     (17, 22, 25, 27),
567     (18, 6, 14, 24),
568     (20, 8, 2, 11),
569     (21, 14, 6, 13),
570     (21, 15, 29, 30),
571     (22, 9, 26, 25),
572     (22, 12, 5, 16),
573     (23, 13, 18),
574     (24, 14, 7, 19),
575     (28, 27, 25, 26),
576     (29, 32, 34, 35),
577     (30, 29, 35, 36),
578     (31, 30, 36, 33),
579     (32, 31, 33, 34),
580     (35, 34, 33, 36),
581     )
582
583
584 # useful since its a shell for solidify and it can be mirrored
585 cube_shell_vertices = (
586     (0, 0, 1),
587     (0, 1, 1),
588     (-1, 1, 1),
589     (-1, 0, 1),
590     (0, 0, 0),
591     (0, 1, 0),
592     (-1, 1, 0),
593     (-1, 0, 0),
594     (-1, -1, 0),
595     (0, -1, 0),
596     (0, 0, -1),
597     (0, 1, -1),
598     )
599
600
601 cube_shell_face = (
602     (0, 1, 2, 3),
603     (0, 3, 8, 9),
604     (1, 5, 6, 2),
605     (2, 6, 7, 3),
606     (3, 7, 8),
607     (4, 7, 10),
608     (6, 5, 11),
609     (7, 4, 9, 8),
610     (10, 7, 6, 11),
611     )
612
613
614 def make_cube(scene):
615     bpy.ops.mesh.primitive_cube_add(view_align=False,
616                                     enter_editmode=False,
617                                     location=(0, 0, 0),
618                                     rotation=(0, 0, 0),
619                                     )
620
621     obj = scene.objects.active
622
623     defaults_object(obj)
624     return obj
625
626
627 def make_cube_extra(scene):
628     obj = make_cube(scene)
629
630     # extra data layers
631     mesh_uv_add(obj)
632     mesh_vcol_add(obj)
633     mesh_vgroup_add(obj)
634
635     return obj
636
637
638 def make_cube_like(scene):
639     mesh = bpy.data.meshes.new(whoami())
640
641     mesh.from_pydata(cube_like_vertices, (), cube_like_faces)
642     mesh.update()  # add edges
643     obj = bpy.data.objects.new(whoami(), mesh)
644     scene.objects.link(obj)
645
646     defaults_object(obj)
647     return obj
648
649
650 def make_cube_like_extra(scene):
651     obj = make_cube_like(scene)
652
653     # extra data layers
654     mesh_uv_add(obj)
655     mesh_vcol_add(obj)
656     mesh_vgroup_add(obj)
657
658     return obj
659
660
661 def make_cube_shell(scene):
662     mesh = bpy.data.meshes.new(whoami())
663
664     mesh.from_pydata(cube_shell_vertices, (), cube_shell_face)
665     mesh.update()  # add edges
666     obj = bpy.data.objects.new(whoami(), mesh)
667     scene.objects.link(obj)
668
669     defaults_object(obj)
670     return obj
671
672
673 def make_cube_shell_extra(scene):
674     obj = make_cube_shell(scene)
675
676     # extra data layers
677     mesh_uv_add(obj)
678     mesh_vcol_add(obj)
679     mesh_vgroup_add(obj)
680
681     return obj
682
683
684 def make_monkey(scene):
685     bpy.ops.mesh.primitive_monkey_add(view_align=False,
686                                       enter_editmode=False,
687                                       location=(0, 0, 0),
688                                       rotation=(0, 0, 0),
689                                       )
690     obj = scene.objects.active
691
692     defaults_object(obj)
693     return obj
694
695
696 def make_monkey_extra(scene):
697     obj = make_monkey(scene)
698
699     # extra data layers
700     mesh_uv_add(obj)
701     mesh_vcol_add(obj)
702     mesh_vgroup_add(obj)
703
704     return obj
705
706
707 # -----------------------------------------------------------------------------
708 # tests (utils)
709
710 global_tests = []
711
712 global_tests.append(("none",
713     (),
714     ))
715
716 # single
717 global_tests.append(("subsurf_single",
718     ((modifier_subsurf_add, dict(levels=2)), ),
719     ))
720
721
722 global_tests.append(("armature_single",
723     ((modifier_armature_add, dict()), ),
724     ))
725
726
727 global_tests.append(("mirror_single",
728     ((modifier_mirror_add, dict()), ),
729     ))
730
731 global_tests.append(("hook_single",
732     ((modifier_hook_add, dict()), ),
733     ))
734
735 global_tests.append(("decimate_single",
736     ((modifier_decimate_add, dict()), ),
737     ))
738
739 global_tests.append(("build_single",
740     ((modifier_build_add, dict()), ),
741     ))
742
743 global_tests.append(("mask_single",
744     ((modifier_mask_add, dict()), ),
745     ))
746
747
748 # combinations
749 global_tests.append(("mirror_subsurf",
750     ((modifier_mirror_add, dict()),
751      (modifier_subsurf_add, dict(levels=2))),
752     ))
753
754 global_tests.append(("solidify_subsurf",
755     ((modifier_solidify_add, dict()),
756      (modifier_subsurf_add, dict(levels=2))),
757     ))
758
759
760 def apply_test(test, scene, obj,
761                render_func=None,
762                render_args=None,
763                render_kwargs=None,
764                ):
765
766     test_name, test_funcs = test
767
768     for cb, kwargs in test_funcs:
769         cb(scene, obj, **kwargs)
770
771     render_kwargs_copy = render_kwargs.copy()
772
773     # add test name in filepath
774     render_kwargs_copy["filepath"] += "_%s" % test_name
775
776     render_func(*render_args, **render_kwargs_copy)
777
778
779 # -----------------------------------------------------------------------------
780 # tests themselves!
781 # having the 'test_' prefix automatically means these functions are called
782 # for testing
783
784
785 def test_cube(context, test):
786     scene = context.scene
787     obj = make_cube_extra(scene)
788     ctx_camera_setup(context, location=(3, 3, 3))
789
790     apply_test(test, scene, obj,
791                render_func=render_gl_all_modes,
792                render_args=(context, obj),
793                render_kwargs=dict(filepath=whoami()))
794
795
796 def test_cube_like(context, test):
797     scene = context.scene
798     obj = make_cube_like_extra(scene)
799     ctx_camera_setup(context, location=(5, 5, 5))
800
801     apply_test(test, scene, obj,
802                render_func=render_gl_all_modes,
803                render_args=(context, obj),
804                render_kwargs=dict(filepath=whoami()))
805
806
807 def test_cube_shell(context, test):
808     scene = context.scene
809     obj = make_cube_shell_extra(scene)
810     ctx_camera_setup(context, location=(4, 4, 4))
811
812     apply_test(test, scene, obj,
813                render_func=render_gl_all_modes,
814                render_args=(context, obj),
815                render_kwargs=dict(filepath=whoami()))
816
817
818 # -----------------------------------------------------------------------------
819 # call all tests
820
821 def main():
822     print("Calling main!")
823     #render_gl(bpy.context, "/testme")
824     #ctx_clear_scene()
825
826     context = bpy.context
827
828     ctx_clear_scene()
829
830     # run all tests
831     for key, val in sorted(globals().items()):
832         if key.startswith("test_") and hasattr(val, "__call__"):
833             print("calling:", key)
834             for t in global_tests:
835                 val(context, test=t)
836                 ctx_clear_scene()
837
838
839 # -----------------------------------------------------------------------------
840 # annoying workaround for theme initialization
841
842 if __name__ == "__main__":
843     import bpy
844     from bpy.app.handlers import persistent
845
846     @persistent
847     def load_handler(dummy):
848         print("Load Handler:", bpy.data.filepath)
849         if load_handler.first == False:
850             bpy.app.handlers.scene_update_post.remove(load_handler)
851             try:
852                 main()
853                 import sys
854                 sys.exit(0)
855             except:
856                 import traceback
857                 traceback.print_exc()
858
859                 import sys
860                 # sys.exit(1)  # comment to debug
861
862         else:
863             load_handler.first = False
864
865     load_handler.first = True
866     bpy.app.handlers.scene_update_post.append(load_handler)