blender/tests/python/render_layer/render_layer_common.py

562 lines
18 KiB
Python
Raw Normal View History

import unittest
# ############################################################
# Layer Collection Crawler
# ############################################################
def listbase_iter(data, struct, listbase):
element = data.get_pointer((struct, listbase, b'first'))
while element is not None:
yield element
element = element.get_pointer(b'next')
def linkdata_iter(collection, data):
element = collection.get_pointer((data, b'first'))
while element is not None:
yield element
element = element.get_pointer(b'next')
def get_layer_collection(layer_collection):
data = {}
flag = layer_collection.get(b'flag')
data['is_visible'] = (flag & (1 << 0)) != 0;
data['is_selectable'] = (flag & (1 << 1)) != 0;
data['is_folded'] = (flag & (1 << 2)) != 0;
scene_collection = layer_collection.get_pointer(b'scene_collection')
if scene_collection is None:
name = 'Fail!'
else:
name = scene_collection.get(b'name')
data['name'] = name
objects = []
for link in linkdata_iter(layer_collection, b'object_bases'):
ob_base = link.get_pointer(b'data')
ob = ob_base.get_pointer(b'object')
objects.append(ob.get((b'id', b'name'))[2:])
data['objects'] = objects
collections = {}
for nested_layer_collection in linkdata_iter(layer_collection, b'layer_collections'):
subname, subdata = get_layer_collection(nested_layer_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_layer(layer):
data = {}
name = layer.get(b'name')
data['name'] = name
data['active_object'] = layer.get((b'basact', b'object', b'id', b'name'))[2:]
data['engine'] = layer.get(b'engine')
objects = []
for link in linkdata_iter(layer, b'object_bases'):
ob = link.get_pointer(b'object')
objects.append(ob.get((b'id', b'name'))[2:])
data['objects'] = objects
collections = {}
for layer_collection in linkdata_iter(layer, b'layer_collections'):
subname, subdata = get_layer_collection(layer_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_layers(scene):
"""Return all the render layers and their data"""
layers = {}
for layer in linkdata_iter(scene, b'render_layers'):
name, data = get_layer(layer)
layers[name] = data
return layers
def get_scene_collection_objects(collection, listbase):
objects = []
for link in linkdata_iter(collection, listbase):
ob = link.get_pointer(b'data')
if ob is None:
name = 'Fail!'
else:
name = ob.get((b'id', b'name'))[2:]
objects.append(name)
return objects
def get_scene_collection(collection):
""""""
data = {}
name = collection.get(b'name')
data['name'] = name
data['filter'] = collection.get(b'filter')
data['objects'] = get_scene_collection_objects(collection, b'objects')
data['filter_objects'] = get_scene_collection_objects(collection, b'filter_objects')
collections = {}
for nested_collection in linkdata_iter(collection, b'scene_collections'):
subname, subdata = get_scene_collection(nested_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_scene_collections(scene):
"""Return all the scene collections ahd their data"""
master_collection = scene.get_pointer(b'collection')
return get_scene_collection(master_collection)
def query_scene(filepath, name, callbacks):
"""Return the equivalent to bpy.context.scene"""
import blendfile
with blendfile.open_blend(filepath) as blend:
scenes = [block for block in blend.blocks if block.code == b'SC']
for scene in scenes:
if scene.get((b'id', b'name'))[2:] == name:
output = []
for callback in callbacks:
output.append(callback(scene))
return output
# ############################################################
# Utils
# ############################################################
def import_blendfile():
import bpy
import os, sys
path = os.path.join(
bpy.utils.resource_path('LOCAL'),
'scripts',
'addons',
'io_blend_utils',
'blend',
)
if path not in sys.path:
sys.path.append(path)
def dump(data):
import json
return json.dumps(
data,
sort_keys=True,
indent=4,
separators=(',', ': '),
)
# ############################################################
# Tests
# ############################################################
PDB = False
DUMP_DIFF = True
def compare_files(file_a, file_b):
import filecmp
if not filecmp.cmp(
file_a,
file_b):
if DUMP_DIFF:
import subprocess
subprocess.call(["diff", "-u", file_a, file_b])
if PDB:
import pdb
print("Files differ:", file_a, file_b)
pdb.set_trace()
return False
return True
class RenderLayerTesting(unittest.TestCase):
_test_simple = False
_extra_arguments = []
@classmethod
def setUpClass(cls):
"""Runs once"""
cls.pretest_import_blendfile()
cls.pretest_parsing()
@classmethod
def get_root(cls):
"""
return the folder with the test files
"""
arguments = {}
for argument in cls._extra_arguments:
name, value = argument.split('=')
cls.assertTrue(name and name.startswith("--"), "Invalid argument \"{0}\"".format(argument))
cls.assertTrue(value, "Invalid argument \"{0}\"".format(argument))
arguments[name[2:]] = value.strip('"')
return arguments.get('testdir')
@classmethod
def pretest_parsing(cls):
"""
Test if the arguments are properly set, and store ROOT
name has extra _ because we need this test to run first
"""
root = cls.get_root()
cls.assertTrue(root, "Testdir not set")
@staticmethod
def pretest_import_blendfile():
"""
Make sure blendfile imports with no problems
name has extra _ because we need this test to run first
"""
import_blendfile()
import blendfile
def setUp(self):
"""Runs once per test"""
import bpy
bpy.ops.wm.read_factory_settings()
def path_exists(self, filepath):
import os
self.assertTrue(
os.path.exists(filepath),
"Test file \"{0}\" not found".format(filepath))
def do_object_add(self, filepath_json, add_mode):
"""
Testing for adding objects and see if they
go to the right collection
"""
import bpy
import os
import tempfile
import filecmp
ROOT = self.get_root()
with tempfile.TemporaryDirectory() as dirpath:
filepath_layers = os.path.join(ROOT, 'layers.blend')
# open file
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
# create sub-collections
three_b = bpy.data.objects.get('T.3b')
three_c = bpy.data.objects.get('T.3c')
scene = bpy.context.scene
subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
scorpion = subzero.collections.new('scorpion')
subzero.objects.link(three_b)
scorpion.objects.link(three_c)
layer = scene.render_layers.new('Fresh new Layer')
layer.collections.link(subzero)
# change active collection
layer.collections.active_index = 3
self.assertEqual(layer.collections.active.name, 'scorpion', "Run: test_syncing_object_add")
# change active layer
override = bpy.context.copy()
override["render_layer"] = layer
override["scene_collection"] = layer.collections.active.collection
# add new objects
if add_mode == 'EMPTY':
bpy.ops.object.add(override) # 'Empty'
elif add_mode == 'CYLINDER':
bpy.ops.mesh.primitive_cylinder_add(override) # 'Cylinder'
elif add_mode == 'TORUS':
bpy.ops.mesh.primitive_torus_add(override) # 'Torus'
# save file
filepath_objects = os.path.join(dirpath, 'objects.blend')
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_objects)
# get the generated json
datas = query_scene(filepath_objects, 'Main', (get_scene_collections, get_layers))
self.assertTrue(datas, "Data is not valid")
filepath_objects_json = os.path.join(dirpath, "objects.json")
with open(filepath_objects_json, "w") as f:
for data in datas:
f.write(dump(data))
self.assertTrue(compare_files(
filepath_objects_json,
filepath_json,
),
"Scene dump files differ")
def do_object_add_no_collection(self, add_mode):
"""
Test for adding objects when no collection
exists in render layer
"""
import bpy
# empty layer of collections
layer = bpy.context.render_layer
while layer.collections:
layer.collections.unlink(layer.collections[0])
# add new objects
if add_mode == 'EMPTY':
bpy.ops.object.add() # 'Empty'
elif add_mode == 'CYLINDER':
bpy.ops.mesh.primitive_cylinder_add() # 'Cylinder'
elif add_mode == 'TORUS':
bpy.ops.mesh.primitive_torus_add() # 'Torus'
self.assertEqual(len(layer.collections), 1, "New collection not created")
collection = layer.collections[0]
self.assertEqual(len(collection.objects), 1, "New collection is empty")
def do_object_link(self, master_collection):
import bpy
self.assertEqual(master_collection.name, "Master Collection")
self.assertEqual(master_collection, bpy.context.scene.master_collection)
master_collection.objects.link(bpy.data.objects.new('object', None))
def cleanup_tree(self):
"""
Remove any existent layer and collections,
leaving only the one render_layer we can't remove
"""
import bpy
scene = bpy.context.scene
while len(scene.render_layers) > 1:
scene.render_layers.remove(scene.render_layers[1])
layer = scene.render_layers[0]
while layer.collections:
layer.collections.unlink(layer.collections[0])
master_collection = scene.master_collection
while master_collection.collections:
master_collection.collections.remove(master_collection.collections[0])
class MoveSceneCollectionTesting(RenderLayerTesting):
"""
To be used by tests of render_layer_move_into_scene_collection
"""
def get_initial_scene_tree_map(self):
collections_map = [
['A', [
['i', None],
['ii', None],
['iii', None],
]],
['B', None],
['C', [
['1', None],
['2', None],
['3', [
['dog', None],
['cat', None],
]],
]],
]
return collections_map
def build_scene_tree(self, tree_map, collection=None, ret_dict=None):
"""
Returns a flat dictionary with new scene collections
created from a nested tuple of nested tuples (name, tuple)
"""
import bpy
if collection is None:
collection = bpy.context.scene.master_collection
if ret_dict is None:
ret_dict = {collection.name: collection}
self.assertEqual(collection.name, "Master Collection")
for name, nested_collections in tree_map:
new_collection = collection.collections.new(name)
ret_dict[name] = new_collection
if nested_collections:
self.build_scene_tree(nested_collections, new_collection, ret_dict)
return ret_dict
def setup_tree(self):
"""
Cleanup file, and populate it with class scene tree map
"""
self.cleanup_tree()
self.assertTrue(hasattr(self, "get_initial_scene_tree_map"), "Test class has no get_initial_scene_tree_map method implemented")
return self.build_scene_tree(self.get_initial_scene_tree_map())
def get_scene_tree_map(self, collection=None, ret_list=None):
"""
Extract the scene collection tree from scene
Return as a nested list of nested lists (name, list)
"""
import bpy
if collection is None:
scene = bpy.context.scene
collection = scene.master_collection
if ret_list is None:
ret_list = []
for nested_collection in collection.collections:
new_collection = [nested_collection.name, None]
ret_list.append(new_collection)
if nested_collection.collections:
new_collection[1] = list()
self.get_scene_tree_map(nested_collection, new_collection[1])
return ret_list
def compare_tree_maps(self):
"""
Compare scene with expected (class defined) data
"""
self.assertEqual(self.get_scene_tree_map(), self.get_reference_scene_tree_map())
class MoveSceneCollectionSyncTesting(MoveSceneCollectionTesting):
"""
To be used by tests of render_layer_move_into_scene_collection_sync
"""
def get_initial_layers_tree_map(self):
layers_map = [
['Layer 1', [
'Master Collection',
'C',
'3',
]],
['Layer 2', [
'C',
'3',
'dog',
'cat',
]],
]
return layers_map
def get_reference_layers_tree_map(self):
"""
For those classes we don't expect any changes in the layer tree
"""
return self.get_initial_layers_tree_map()
def setup_tree(self):
tree = super(MoveSceneCollectionSyncTesting, self).setup_tree()
import bpy
scene = bpy.context.scene
self.assertTrue(hasattr(self, "get_initial_layers_tree_map"), "Test class has no get_initial_layers_tree_map method implemented")
layers_map = self.get_initial_layers_tree_map()
for layer_name, collections_names in layers_map:
layer = scene.render_layers.new(layer_name)
layer.collections.unlink(layer.collections[0])
for collection_name in collections_names:
layer.collections.link(tree[collection_name])
return tree
def compare_tree_maps(self):
"""
Compare scene with expected (class defined) data
"""
super(MoveSceneCollectionSyncTesting, self).compare_tree_maps()
import bpy
scene = bpy.context.scene
layers_map = self.get_reference_layers_tree_map()
for layer_name, collections_names in layers_map:
layer = scene.render_layers.get(layer_name)
self.assertTrue(layer)
self.assertEqual(len(collections_names), len(layer.collections))
for i, collection_name in enumerate(collections_names):
self.assertEqual(collection_name, layer.collections[i].name)
self.verify_collection_tree(layer.collections[i])
def verify_collection_tree(self, layer_collection):
"""
Check if the LayerCollection mimics the SceneLayer tree
"""
scene_collection = layer_collection.collection
self.assertEqual(len(layer_collection.collections), len(scene_collection.collections))
for i, nested_collection in enumerate(layer_collection.collections):
self.assertEqual(nested_collection.collection.name, scene_collection.collections[i].name)
self.assertEqual(nested_collection.collection, scene_collection.collections[i])
self.verify_collection_tree(nested_collection)
class MoveLayerCollectionTesting(MoveSceneCollectionSyncTesting):
"""
To be used by tests of render_layer_move_into_layer_collection
"""
def parse_move(self, path, sep='.'):
"""
convert 'Layer 1.C.2' into:
bpy.context.scene.render_layers['Layer 1'].collections['C'].collections['2']
"""
import bpy
paths = path.split(sep)
layer = bpy.context.scene.render_layers[paths[0]]
collections = layer.collections
for subpath in paths[1:]:
collection = collections[subpath]
collections = collection.collections
return collection
def move_into(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_into(layer_collection_dst)
def move_above(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_above(layer_collection_dst)
def move_below(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_below(layer_collection_dst)