forked from bartvdbraak/blender
d9f42e5fab
This is to be used from the Outliner, when dragging and dropping collections from the Active Render Layer It also includes a cleanup on the outliner so it calls the new functions. Note: the outliner still needs fix to allow all the functionality here exposed. But this will be tackled by Julian Eisel later.
562 lines
18 KiB
Python
562 lines
18 KiB
Python
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)
|
|
|