823 lines
26 KiB
Python
823 lines
26 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import unittest
|
|
|
|
__all__ = (
|
|
"Clay",
|
|
"MoveLayerCollectionTesting",
|
|
"MoveSceneCollectionSyncTesting",
|
|
"MoveSceneCollectionTesting",
|
|
"ViewLayerTesting",
|
|
"compare_files",
|
|
"dump",
|
|
"get_layers",
|
|
"get_scene_collections",
|
|
"query_scene",
|
|
"setup_extra_arguments",
|
|
)
|
|
|
|
# ############################################################
|
|
# 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_disabled'] = (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(scene, layer):
|
|
data = {}
|
|
name = layer.get(b'name')
|
|
|
|
data['name'] = name
|
|
data['engine'] = scene.get((b'r', b'engine'))
|
|
|
|
active_base = layer.get_pointer(b'basact')
|
|
if active_base:
|
|
ob = active_base.get_pointer(b'object')
|
|
data['active_object'] = ob.get((b'id', b'name'))[2:]
|
|
else:
|
|
data['active_object'] = ""
|
|
|
|
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'view_layers'):
|
|
name, data = get_layer(scene, 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['objects'] = get_scene_collection_objects(collection, b'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"""
|
|
from io_blend_utils.blend 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:
|
|
continue
|
|
|
|
return [callback(scene) for callback in callbacks]
|
|
|
|
|
|
# ############################################################
|
|
# Utils
|
|
# ############################################################
|
|
|
|
def dump(data):
|
|
import json
|
|
return json.dumps(
|
|
data,
|
|
sort_keys=True,
|
|
indent=4,
|
|
separators=(',', ': '),
|
|
)
|
|
|
|
|
|
# ############################################################
|
|
# Tests
|
|
# ############################################################
|
|
|
|
PDB = False
|
|
DUMP_DIFF = True
|
|
UPDATE_DIFF = False # HACK used to update tests when something change
|
|
|
|
|
|
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_b, file_a])
|
|
|
|
if UPDATE_DIFF:
|
|
import subprocess
|
|
subprocess.call(["cp", "-u", file_a, file_b])
|
|
|
|
if PDB:
|
|
import pdb
|
|
print("Files differ:", file_b, file_a)
|
|
pdb.set_trace()
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class ViewLayerTesting(unittest.TestCase):
|
|
_test_simple = False
|
|
_extra_arguments = []
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
"""Runs once"""
|
|
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")
|
|
|
|
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)
|
|
self.rename_collections()
|
|
|
|
# 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.view_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["view_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.view_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 do_scene_copy(self, filepath_json_reference, copy_mode, data_callbacks):
|
|
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')
|
|
|
|
(self.path_exists(f) for f in (
|
|
filepath_layers,
|
|
filepath_json_reference,
|
|
))
|
|
|
|
filepath_saved = os.path.join(dirpath, '{0}.blend'.format(copy_mode))
|
|
filepath_json = os.path.join(dirpath, "{0}.json".format(copy_mode))
|
|
|
|
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
|
|
self.rename_collections()
|
|
bpy.ops.scene.new(type=copy_mode)
|
|
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_saved)
|
|
|
|
datas = query_scene(filepath_saved, 'Main.001', data_callbacks)
|
|
self.assertTrue(datas, "Data is not valid")
|
|
|
|
with open(filepath_json, "w") as f:
|
|
for data in datas:
|
|
f.write(dump(data))
|
|
|
|
self.assertTrue(compare_files(
|
|
filepath_json,
|
|
filepath_json_reference,
|
|
),
|
|
"Scene copy \"{0}\" test failed".format(copy_mode.title()))
|
|
|
|
def do_object_delete(self, del_mode):
|
|
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')
|
|
filepath_reference_json = os.path.join(ROOT, 'layers_object_delete.json')
|
|
|
|
# open file
|
|
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
|
|
self.rename_collections()
|
|
|
|
# create sub-collections
|
|
three_b = bpy.data.objects.get('T.3b')
|
|
three_d = bpy.data.objects.get('T.3d')
|
|
|
|
scene = bpy.context.scene
|
|
|
|
# mangle the file a bit with some objects linked across collections
|
|
subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
|
|
scorpion = subzero.collections.new('scorpion')
|
|
subzero.objects.link(three_d)
|
|
scorpion.objects.link(three_b)
|
|
scorpion.objects.link(three_d)
|
|
|
|
# object to delete
|
|
ob = three_d
|
|
|
|
# delete object
|
|
if del_mode == 'DATA':
|
|
bpy.data.objects.remove(ob, do_unlink=True)
|
|
|
|
elif del_mode == 'OPERATOR':
|
|
bpy.context.view_layer.update() # update depsgraph
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
ob.select_set(True)
|
|
self.assertTrue(ob.select_get())
|
|
bpy.ops.object.delete()
|
|
|
|
# save file
|
|
filepath_generated = os.path.join(dirpath, 'generated.blend')
|
|
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_generated)
|
|
|
|
# get the generated json
|
|
datas = query_scene(filepath_generated, 'Main', (get_scene_collections, get_layers))
|
|
self.assertTrue(datas, "Data is not valid")
|
|
|
|
filepath_generated_json = os.path.join(dirpath, "generated.json")
|
|
with open(filepath_generated_json, "w") as f:
|
|
for data in datas:
|
|
f.write(dump(data))
|
|
|
|
self.assertTrue(compare_files(
|
|
filepath_generated_json,
|
|
filepath_reference_json,
|
|
),
|
|
"Scene dump files differ")
|
|
|
|
def do_visibility_object_add(self, add_mode):
|
|
import bpy
|
|
|
|
scene = bpy.context.scene
|
|
|
|
# delete all objects of the file
|
|
for ob in bpy.data.objects:
|
|
bpy.data.objects.remove(ob, do_unlink=True)
|
|
|
|
# real test
|
|
layer = scene.view_layers.new('Visibility Test')
|
|
layer.collections.unlink(layer.collections[0])
|
|
|
|
scene_collection = scene.master_collection.collections.new("Collection")
|
|
layer.collections.link(scene_collection)
|
|
|
|
bpy.context.view_layer.update() # update depsgraph
|
|
|
|
self.assertEqual(len(bpy.data.objects), 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(bpy.data.objects), 1)
|
|
|
|
new_ob = bpy.data.objects[0]
|
|
self.assertTrue(new_ob.visible_get(), "Object should be visible")
|
|
|
|
def cleanup_tree(self):
|
|
"""
|
|
Remove any existent layer and collections,
|
|
leaving only the one view_layer we can't remove
|
|
"""
|
|
import bpy
|
|
scene = bpy.context.scene
|
|
while len(scene.view_layers) > 1:
|
|
scene.view_layers.remove(scene.view_layers[1])
|
|
|
|
layer = scene.view_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])
|
|
|
|
def rename_collections(self, collection=None):
|
|
"""
|
|
Rename 'Collection 1' to '1'
|
|
"""
|
|
def strip_name(collection):
|
|
import re
|
|
if collection.name.startswith("Default Collection"):
|
|
collection.name = '1'
|
|
else:
|
|
collection.name = re.findall(r'\d+', collection.name)[0]
|
|
|
|
if collection is None:
|
|
import bpy
|
|
collection = bpy.context.scene.master_collection
|
|
|
|
for nested_collection in collection.collections:
|
|
strip_name(nested_collection)
|
|
self.rename_collections(nested_collection)
|
|
|
|
|
|
class MoveSceneCollectionTesting(ViewLayerTesting):
|
|
"""
|
|
To be used by tests of view_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 view_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.view_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.view_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 view_layer_move_into_layer_collection
|
|
"""
|
|
|
|
def parse_move(self, path, sep='.'):
|
|
"""
|
|
convert 'Layer 1.C.2' into:
|
|
bpy.context.scene.view_layers['Layer 1'].collections['C'].collections['2']
|
|
"""
|
|
import bpy
|
|
|
|
paths = path.split(sep)
|
|
layer = bpy.context.scene.view_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)
|
|
|
|
|
|
class Clay:
|
|
def __init__(self, extra_kid_layer=False):
|
|
import bpy
|
|
|
|
self._scene = bpy.context.scene
|
|
self._layer = self._fresh_layer()
|
|
self._object = bpy.data.objects.new('guinea pig', bpy.data.meshes.new('mesh'))
|
|
|
|
# update depsgraph
|
|
self._layer.update()
|
|
|
|
scene_collection_grandma = self._scene.master_collection.collections.new("Grandma")
|
|
scene_collection_mom = scene_collection_grandma.collections.new("Mom")
|
|
scene_collection_kid = scene_collection_mom.collections.new("Kid")
|
|
scene_collection_kid.objects.link(self._object)
|
|
|
|
layer_collection_grandma = self._layer.collections.link(scene_collection_grandma)
|
|
layer_collection_mom = layer_collection_grandma.collections[0]
|
|
layer_collection_kid = layer_collection_mom.collections[0]
|
|
|
|
# store the variables
|
|
self._scene_collections = {
|
|
'grandma': scene_collection_grandma,
|
|
'mom': scene_collection_mom,
|
|
'kid': scene_collection_kid,
|
|
}
|
|
self._layer_collections = {
|
|
'grandma': layer_collection_grandma,
|
|
'mom': layer_collection_mom,
|
|
'kid': layer_collection_kid,
|
|
}
|
|
|
|
if extra_kid_layer:
|
|
layer_collection_extra = self._layer.collections.link(scene_collection_kid)
|
|
self._layer_collections['extra'] = layer_collection_extra
|
|
|
|
self._update()
|
|
|
|
def _fresh_layer(self):
|
|
import bpy
|
|
|
|
# remove all other objects
|
|
while bpy.data.objects:
|
|
bpy.data.objects.remove(bpy.data.objects[0])
|
|
|
|
# remove all the other collections
|
|
while self._scene.master_collection.collections:
|
|
self._scene.master_collection.collections.remove(
|
|
self._scene.master_collection.collections[0])
|
|
|
|
layer = self._scene.view_layers.new('Evaluation Test')
|
|
layer.collections.unlink(layer.collections[0])
|
|
bpy.context.window.view_layer = layer
|
|
|
|
# remove all other layers
|
|
for layer_iter in self._scene.view_layers:
|
|
if layer_iter != layer:
|
|
self._scene.view_layers.remove(layer_iter)
|
|
|
|
return layer
|
|
|
|
def _update(self):
|
|
"""
|
|
Force depsgrpah evaluation
|
|
and update pointers to IDProperty collections
|
|
"""
|
|
ENGINE = 'BLENDER_CLAY'
|
|
|
|
self._layer.update() # flush depsgraph evaluation
|
|
|
|
# change scene settings
|
|
self._properties = {
|
|
'scene': self._scene.collection_properties[ENGINE],
|
|
'object': self._object.collection_properties[ENGINE],
|
|
}
|
|
|
|
for key, value in self._layer_collections.items():
|
|
self._properties[key] = self._layer_collections[key].engine_overrides[ENGINE]
|
|
|
|
def get(self, name, data_path):
|
|
self._update()
|
|
return getattr(self._properties[name], data_path)
|
|
|
|
def set(self, name, data_path, value):
|
|
self._update()
|
|
self._properties[name].use(data_path)
|
|
setattr(self._properties[name], data_path, value)
|
|
|
|
|
|
def setup_extra_arguments(filepath):
|
|
"""
|
|
Create a value which is assigned to: ``UnitTesting._extra_arguments``
|
|
"""
|
|
import sys
|
|
|
|
extra_arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
sys.argv = [filepath] + extra_arguments[1:]
|
|
|
|
return extra_arguments
|