Anim: emit liboverride on slot handle when action is changed
Emit a 'diff' for the `animdata.slot_handle` property whenever the `.action` property is changed through a library override. The slot handle is only meaningful within the context of the assigned action. So when a liboverride changes the assigned action, the slot handle should also get an override. This is necessary even when the numerical value of the slot handle happens to be the same in both actions, as the newly chosen slot is different from the slot that was chosen in the library file. This applies to direct Action assignment, NLA strips, and Action constraints. Pull Request: https://projects.blender.org/blender/blender/pulls/133727
This commit is contained in:
@@ -476,6 +476,7 @@ add_blender_test(
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_action.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/animation"
|
||||
--output-dir "${TEST_OUT_DIR}/bl_animation_action"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
|
||||
@@ -613,6 +613,114 @@ class VersioningTest(unittest.TestCase):
|
||||
self.assertEqual(fcurve.group.name, "Bone.001")
|
||||
|
||||
|
||||
class SlotHandleLibraryOverridesTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cls.libfile = args.testdir.resolve() / "liboverride-action-slot-libfile.blend"
|
||||
cls.workfile = args.output_dir.resolve() / "liboverride-action-slot.blend"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.workfile.unlink(missing_ok=True)
|
||||
|
||||
def test_liboverride_slot_handle(self):
|
||||
# Whenever a liboverride changes the assigned Action, there should be a
|
||||
# liboverride on the slot handle as well. Even when the assigned slot in
|
||||
# the original data numerically has the same handle as the overridden
|
||||
# slot.
|
||||
|
||||
self._create_test_file()
|
||||
self._load_test_file()
|
||||
self._check_assumptions()
|
||||
self._perform_test()
|
||||
|
||||
def _create_test_file(self):
|
||||
"""Create the test file.
|
||||
|
||||
This has to happen every time the test runs, because it's about the
|
||||
creation of library override operations. Creating the file once, storing
|
||||
it with the rest of the test files, and opening it here to test it, will
|
||||
just repeat the test on a once-written-correctly file, and not test the
|
||||
currently-running Blender.
|
||||
"""
|
||||
|
||||
bpy.ops.wm.read_homefile(use_factory_startup=True, use_empty=True)
|
||||
|
||||
# Link Suzanne into the file and then into the scene.
|
||||
with bpy.data.libraries.load(str(self.libfile), link=True, relative=False) as (data_from, data_to):
|
||||
data_to.objects = ['Library Suzanne']
|
||||
orig_lib_suzanne = data_to.objects[0]
|
||||
bpy.context.scene.collection.objects.link(orig_lib_suzanne)
|
||||
|
||||
# Create a library override on Suzanne.
|
||||
with bpy.context.temp_override(active_object=orig_lib_suzanne):
|
||||
bpy.ops.object.make_override_library()
|
||||
|
||||
# Create a local Action to assign.
|
||||
local_action = bpy.data.actions.new("Local Action")
|
||||
local_slot = local_action.slots.new('OBJECT', "Local Slot")
|
||||
layer = local_action.layers.new("Layer")
|
||||
strip = layer.strips.new(type='KEYFRAME')
|
||||
cbag = strip.channelbags.new(local_slot)
|
||||
fcurve = cbag.fcurves.new('location', index=2)
|
||||
fcurve.keyframe_points.insert(1, -5)
|
||||
fcurve.keyframe_points.insert(20, 5)
|
||||
|
||||
# Grab the overridden Suzanne, and assign the local Action + a slot from that Action.
|
||||
override_suzanne = bpy.data.objects['Library Suzanne', None]
|
||||
override_suzanne.animation_data.action = local_action
|
||||
override_suzanne.animation_data.action_slot = local_slot
|
||||
|
||||
# Save the file to disk.
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(self.workfile), check_existing=False)
|
||||
|
||||
def _load_test_file(self):
|
||||
bpy.ops.wm.read_homefile(use_factory_startup=True) # Just to be sure.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.workfile), load_ui=False)
|
||||
|
||||
def _check_assumptions(self):
|
||||
"""Check that the test data is indeed as expected."""
|
||||
|
||||
# The library Action and the local Action should have the same handle on
|
||||
# the first slot. If the slot handles are different, Blender's default
|
||||
# library override diffing code would create an override operation, and
|
||||
# this test will produce a false positive.
|
||||
self.assertEqual(
|
||||
bpy.data.actions['Library Action'].slots[0].handle,
|
||||
bpy.data.actions['Local Action'].slots[0].handle,
|
||||
)
|
||||
|
||||
# The library & local Action slots should have different identifiers.
|
||||
# Otherwise the slot assignment will be correct regardless of library
|
||||
# overrides, and this test will produce a false positive.
|
||||
self.assertNotEqual(
|
||||
bpy.data.actions['Library Action'].slots[0].identifier,
|
||||
bpy.data.actions['Local Action'].slots[0].identifier,
|
||||
)
|
||||
|
||||
# Check the Action assignments before we trust a check for the action slot.
|
||||
libpath = bpy.data.libraries['liboverride-action-slot-libfile.blend'].filepath
|
||||
orig_lib_suzanne = bpy.data.objects['Library Suzanne', libpath]
|
||||
override_suzanne = bpy.data.objects['Library Suzanne', None]
|
||||
|
||||
self.assertEqual(bpy.data.actions['Library Action'], orig_lib_suzanne.animation_data.action)
|
||||
self.assertEqual(bpy.data.actions['Local Action'], override_suzanne.animation_data.action)
|
||||
|
||||
def _perform_test(self):
|
||||
override_suzanne = bpy.data.objects['Library Suzanne', None]
|
||||
|
||||
# === The actual test ===
|
||||
self.assertEqual(bpy.data.actions['Local Action'].slots[0], override_suzanne.animation_data.action_slot)
|
||||
|
||||
# Set Suzanne's Z position to something large, and go the first frame to
|
||||
# let the animation system evaluation overwrite it.
|
||||
bpy.context.scene.frame_set(1)
|
||||
self.assertLess(override_suzanne.location.z,
|
||||
-1, "Suzanne should be significantly below Z=0 when animated by the library Action")
|
||||
|
||||
|
||||
def main():
|
||||
global args
|
||||
import argparse
|
||||
@@ -623,6 +731,15 @@ def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--testdir', required=True, type=pathlib.Path)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
dest="output_dir",
|
||||
type=pathlib.Path,
|
||||
default=pathlib.Path("."),
|
||||
help="Where to output temp saved blendfiles",
|
||||
required=False,
|
||||
)
|
||||
|
||||
args, remaining = parser.parse_known_args(argv)
|
||||
|
||||
unittest.main(argv=remaining)
|
||||
|
||||
Reference in New Issue
Block a user