Refactor: adjust unit tests to no longer use the legacy Action API

Adjust various unit tests so that they no longer use the legacy Action
API (which was deprecated in Blender 4.4 and will be removed in 5.0).

No functional changes.

This is part of #146586

Pull Request: https://projects.blender.org/blender/blender/pulls/147060
This commit is contained in:
Sybren A. Stüvel
2025-09-29 12:42:32 +02:00
parent e4b4e7c308
commit 3171a22dfd
6 changed files with 167 additions and 302 deletions

View File

@@ -377,203 +377,6 @@ class LimitationsTest(unittest.TestCase):
self.assertFalse(hasattr(strip, 'frame_offset'))
class LegacyAPIOnLayeredActionTest(unittest.TestCase):
"""Test that the legacy Action API works on layered Actions.
It should give access to the keyframes for the first slot.
- curve_frame_range
- fcurves
- groups
- id_root
- flip_with_pose(object)
"""
def setUp(self) -> None:
bpy.ops.wm.read_homefile(use_factory_startup=True)
self.action = bpy.data.actions.new('LayeredAction')
def test_fcurves_on_layered_action(self) -> None:
slot = self.action.slots.new(bpy.data.objects['Cube'].id_type, bpy.data.objects['Cube'].name)
layer = self.action.layers.new(name="Layer")
strip = layer.strips.new(type='KEYFRAME')
channelbag = strip.channelbags.new(slot=slot)
# Create new F-Curves via legacy API, they should be stored on the Channelbag.
fcurve1 = self.action.fcurves.new("scale", index=1)
fcurve2 = self.action.fcurves.new("scale", index=2)
self.assertEqual([fcurve1, fcurve2], channelbag.fcurves[:], "Expected two F-Curves after creating them")
self.assertEqual([fcurve1, fcurve2], self.action.fcurves[:],
"Expected the same F-Curves on the legacy API")
# Find an F-Curve.
self.assertEqual(fcurve2, self.action.fcurves.find("scale", index=2))
# Create an already-existing F-Curve.
try:
self.action.fcurves.new("scale", index=2)
except RuntimeError as ex:
self.assertIn("F-Curve 'scale[2]' already exists in action 'LayeredAction'", str(ex))
else:
self.fail("expected RuntimeError not thrown")
self.assertEqual([fcurve1, fcurve2], channelbag.fcurves[:],
"Expected two F-Curves after failing to create a third")
self.assertEqual([fcurve1, fcurve2], self.action.fcurves[:])
# Remove a single F-Curve.
self.action.fcurves.remove(fcurve1)
self.assertEqual([fcurve2], channelbag.fcurves[:], "Expected single F-Curve after removing one")
self.assertEqual([fcurve2], self.action.fcurves[:])
# Clear all F-Curves (with multiple F-Curves to avoid the trivial case).
self.action.fcurves.new("scale", index=3)
self.action.fcurves.clear()
self.assertEqual([], channelbag.fcurves[:], "Expected empty fcurves list after clearing")
self.assertEqual([], self.action.fcurves[:])
def test_fcurves_clear_should_not_create_layers(self):
self.action.fcurves.clear()
self.assertEqual([], self.action.slots[:])
self.assertEqual([], self.action.layers[:])
def test_fcurves_new_on_empty_action(self) -> None:
# Create new F-Curves via legacy API, this should create a layer+strip+Channelbag.
fcurve1 = self.action.fcurves.new("scale", index=1)
fcurve2 = self.action.fcurves.new("scale", index=2)
self.assertEqual(1, len(self.action.slots))
self.assertEqual(1, len(self.action.layers))
slot = self.action.slots[0]
layer = self.action.layers[0]
self.assertEqual("Legacy Slot", slot.name_display)
self.assertEqual("Legacy Layer", layer.name)
self.assertEqual(1, len(layer.strips))
strip = layer.strips[0]
self.assertEqual('KEYFRAME', strip.type)
self.assertEqual(1, len(strip.channelbags))
channelbag = strip.channelbags[0]
self.assertEqual(channelbag.slot_handle, slot.handle)
self.assertEqual([fcurve1, fcurve2], channelbag.fcurves[:])
# After this, there is no need to test the rest of the functions, as the
# Action will be in the same state as in test_fcurves_on_layered_action().
def test_groups(self) -> None:
# Create a group by using the legacy API to create an F-Curve with group name.
group_name = "Object Transfoibles"
self.action.fcurves.new("scale", index=1, action_group=group_name)
layer = self.action.layers[0]
strip = layer.strips[0]
channelbag = strip.channelbags[0]
self.assertEqual(1, len(channelbag.groups), "The new group should be available on the channelbag")
self.assertEqual(group_name, channelbag.groups[0].name)
self.assertEqual(1, len(self.action.groups), "The new group should be available with the legacy group API")
self.assertEqual(group_name, self.action.groups[0].name)
# Create a group via the legacy API.
group = self.action.groups.new(group_name)
self.assertEqual("{}.001".format(group_name), group.name, "The group should have a unique name")
self.assertEqual(group, self.action.groups[1], "The group should be accessible via the legacy API")
self.assertEqual(group, channelbag.groups[1], "The group should be accessible via the channelbag")
# Remove a group via the legacy API.
self.action.groups.remove(group)
self.assertNotIn(group, self.action.groups[:], "A group should be removable via the legacy API")
self.assertNotIn(group, channelbag.groups[:], "A group should be removable via the legacy API")
def test_groups_new_on_empty_action(self) -> None:
# Create new group via legacy API, this should create a layer+strip+Channelbag.
group = self.action.groups.new("foo")
self.assertEqual(1, len(self.action.slots))
self.assertEqual(1, len(self.action.layers))
slot = self.action.slots[0]
layer = self.action.layers[0]
self.assertEqual("Legacy Slot", slot.name_display)
self.assertEqual("Legacy Layer", layer.name)
self.assertEqual(1, len(layer.strips))
strip = layer.strips[0]
self.assertEqual('KEYFRAME', strip.type)
self.assertEqual(1, len(strip.channelbags))
channelbag = strip.channelbags[0]
self.assertEqual(channelbag.slot_handle, slot.handle)
self.assertEqual([group], channelbag.groups[:])
def test_id_root_on_layered_action(self) -> None:
# When there's at least one slot, action.id_root should simply act as a
# proxy for the first slot's target_id_type. This should work for both
# reading and writing.
slot_1 = self.action.slots.new('OBJECT', "Slot 1")
slot_2 = self.action.slots.new('CAMERA', "Slot 2")
bpy.data.objects['Cube'].animation_data_create()
bpy.data.objects['Cube'].animation_data.action = self.action
bpy.data.objects['Cube'].animation_data.action_slot = slot_1
self.assertEqual(self.action.id_root, 'OBJECT')
self.assertEqual(self.action.slots[0].target_id_type, 'OBJECT')
self.assertEqual(self.action.slots[0].identifier, 'OBSlot 1')
self.assertEqual(self.action.slots[1].target_id_type, 'CAMERA')
self.assertEqual(self.action.slots[1].identifier, 'CASlot 2')
self.assertEqual(bpy.data.objects['Cube'].animation_data.last_slot_identifier, 'OBSlot 1')
self.action.id_root = 'MATERIAL'
self.assertEqual(self.action.id_root, 'MATERIAL')
self.assertEqual(self.action.slots[0].target_id_type, 'MATERIAL')
self.assertEqual(self.action.slots[0].identifier, 'MASlot 1')
self.assertEqual(self.action.slots[1].target_id_type, 'CAMERA')
self.assertEqual(self.action.slots[1].identifier, 'CASlot 2')
self.assertEqual(bpy.data.objects['Cube'].animation_data.last_slot_identifier, 'MASlot 1')
def test_id_root_on_layered_action_for_identifier_uniqueness(self) -> None:
# When setting id_root such that the first slot's identifier would
# become a duplicate, the name portion of the identifier should be
# automatically renamed to be unique.
slot_1 = self.action.slots.new('OBJECT', "Foo")
slot_2 = self.action.slots.new('CAMERA', "Foo")
self.assertEqual(self.action.id_root, 'OBJECT')
self.assertEqual(self.action.slots[0].target_id_type, 'OBJECT')
self.assertEqual(self.action.slots[0].identifier, 'OBFoo')
self.assertEqual(self.action.slots[1].target_id_type, 'CAMERA')
self.assertEqual(self.action.slots[1].identifier, 'CAFoo')
self.action.id_root = 'CAMERA'
self.assertEqual(self.action.id_root, 'CAMERA')
self.assertEqual(self.action.slots[0].target_id_type, 'CAMERA')
self.assertEqual(self.action.slots[0].identifier, 'CAFoo.001')
self.assertEqual(self.action.slots[1].target_id_type, 'CAMERA')
self.assertEqual(self.action.slots[1].identifier, 'CAFoo')
def test_id_root_on_empty_action(self) -> None:
# When there are no slots, setting action.id_root should create a legacy
# slot and set its target_id_type.
self.assertEqual(self.action.id_root, 'UNSPECIFIED')
self.assertEqual(len(self.action.slots), 0)
self.action.id_root = 'OBJECT'
self.assertEqual(self.action.id_root, 'OBJECT')
self.assertEqual(len(self.action.slots), 1)
self.assertEqual(self.action.slots[0].target_id_type, 'OBJECT')
class ChannelbagsTest(unittest.TestCase):
def setUp(self):
anims = bpy.data.actions

View File

@@ -20,16 +20,32 @@ class AbstractAnimationTest:
cls.testdir = args.testdir
def setUp(self):
assert isinstance(self, unittest.TestCase)
self.assertTrue(self.testdir.exists(),
'Test dir %s should exist' % self.testdir)
def _channelbag(animated_id: bpy.types.ID) -> bpy.types.ActionChannelbag:
"""Return the first layer's Channelbag of the animated ID's Action."""
action = animated_id.animation_data.action
action_slot = animated_id.animation_data.action_slot
channelbag = action.layers[0].strips[0].channelbag(action_slot)
assert channelbag is not None
return channelbag
def _first_fcurve(animated_id: bpy.types.ID) -> bpy.types.FCurve:
"""Return the first F-Curve of the animated ID's Action."""
return _channelbag(animated_id).fcurves[0]
class FCurveEvaluationTest(AbstractAnimationTest, unittest.TestCase):
def test_fcurve_versioning_291(self):
# See D8752.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "fcurve-versioning-291.blend"))
cube = bpy.data.objects['Cube']
fcurve = cube.animation_data.action.fcurves.find('location', index=0)
channelbag = cube.animation_data.action.layers[0].strips[0].channelbags[0]
fcurve = channelbag.fcurves.find('location', index=0)
self.assertAlmostEqual(0.0, fcurve.evaluate(1))
self.assertAlmostEqual(0.019638698548078537, fcurve.evaluate(2))
@@ -46,7 +62,8 @@ class FCurveEvaluationTest(AbstractAnimationTest, unittest.TestCase):
# See D8752.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "fcurve-extreme-handles.blend"))
cube = bpy.data.objects['Cube']
fcurve = cube.animation_data.action.fcurves.find('location', index=0)
channelbag = cube.animation_data.action.layers[0].strips[0].channelbags[0]
fcurve = channelbag.fcurves.find('location', index=0)
self.assertAlmostEqual(0.0, fcurve.evaluate(1))
self.assertAlmostEqual(0.004713400732725859, fcurve.evaluate(2))
@@ -66,11 +83,6 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
This tests both the evaluation of the RNA property and the F-Curve
interpolation itself (the not-exposed-to-RNA flags `FCURVE_INT_VALUES` and
`FCURVE_DISCRETE_VALUES` have an impact on the latter as well).
NOTE: This test uses the backward-compatible API in 4.4 (action.fcurves)
because it only uses a single slot anyway. This way, the test is
backward-compatible with older versions of Blender, and can be used to track
down regression issues.
"""
def setUp(self):
@@ -87,7 +99,7 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
camera.keyframe_insert('lens', frame=64)
self._make_all_keys_linear()
fcurve = camera.animation_data.action.fcurves[0]
fcurve = _first_fcurve(camera)
scene.frame_set(0)
self.assertAlmostEqual(16, camera.lens)
@@ -115,7 +127,7 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
render.keyframe_insert('simplify_subdivision', frame=64)
self._make_all_keys_linear()
fcurve = scene.animation_data.action.fcurves[0]
fcurve = _first_fcurve(scene)
scene.frame_set(0)
self.assertAlmostEqual(16, render.simplify_subdivision)
@@ -143,7 +155,7 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
render.keyframe_insert('use_simplify', frame=64)
self._make_all_keys_linear()
fcurve = scene.animation_data.action.fcurves[0]
fcurve = _first_fcurve(scene)
scene.frame_set(0)
self.assertEqual(False, render.use_simplify)
@@ -170,7 +182,7 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
cube.keyframe_insert('rotation_mode', frame=64)
self._make_all_keys_linear()
fcurve = cube.animation_data.action.fcurves[0]
fcurve = _first_fcurve(cube)
scene.frame_set(0)
self.assertEqual('QUATERNION', cube.rotation_mode)
@@ -192,13 +204,13 @@ class PropertyInterpolationTest(AbstractAnimationTest, unittest.TestCase):
"""
for action in bpy.data.actions:
# Make this test backward compatible with older versions of Blender,
# to make it easier to test regressions.
self.assertEqual(1, len(action.slots), f"{action} should have exactly one slot")
for fcurve in action.fcurves:
for key in fcurve.keyframe_points:
key.interpolation = 'LINEAR'
for layer in action.layers:
for strip in layer.strips:
self.assertEqual(strip.type, 'KEYFRAME')
for channelbag in strip.channelbags:
for fcurve in channelbag.fcurves:
for key in fcurve.keyframe_points:
key.interpolation = 'LINEAR'
class EulerFilterTest(AbstractAnimationTest, unittest.TestCase):
@@ -290,8 +302,8 @@ class EulerFilterTest(AbstractAnimationTest, unittest.TestCase):
@staticmethod
def active_object_rotation_channels() -> list[bpy.types.FCurve]:
ob = bpy.context.view_layer.objects.active
action = ob.animation_data.action
return [action.fcurves.find('rotation_euler', index=idx) for idx in range(3)]
channelbag = _channelbag(ob)
return [channelbag.fcurves.find('rotation_euler', index=idx) for idx in range(3)]
def get_view3d_context():
@@ -322,8 +334,9 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
fcurve = _first_fcurve(key_object)
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
key = fcurve.keyframe_points[key_index]
self.assertEqual(key.co.x, key_index)
bpy.ops.object.delete(use_global=False)
@@ -339,7 +352,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
key_object.keyframe_insert("rotation_euler", keytype='UNSUPPORTED')
# Only a single key should have been inserted.
keys = key_object.animation_data.action.fcurves[0].keyframe_points
keys = _first_fcurve(key_object).keyframe_points
self.assertEqual(len(keys), 1)
self.assertEqual(keys[0].type, 'GENERATED')
@@ -354,7 +367,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
key = _first_fcurve(key_object).keyframe_points[key_index]
self.assertEqual(key.co.x, key_index + frame_offset)
bpy.ops.object.delete(use_global=False)
@@ -369,7 +382,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
key = _first_fcurve(key_object).keyframe_points[key_index]
self.assertAlmostEqual(key.co.x, key_index / key_count)
bpy.ops.object.delete(use_global=False)
@@ -405,7 +418,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
# Even though range() is exclusive, the floating point limitations mean keys end up on that position.
1000001.0
]
keyframe_points = key_object.animation_data.action.fcurves[0].keyframe_points
keyframe_points = _first_fcurve(key_object).keyframe_points
for i, value in enumerate(floating_point_steps):
key = keyframe_points[i]
self.assertAlmostEqual(key.co.x, value)
@@ -429,7 +442,7 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
fcu = _first_fcurve(key_object)
for i in range(key_count):
fcu.keyframe_points.insert(frame=i, value=0)
@@ -452,7 +465,7 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
fcu = _first_fcurve(key_object)
for i in range(key_count):
fcu.keyframe_points.insert(frame=i + frame_offset, value=0)
@@ -474,7 +487,7 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
fcu = _first_fcurve(key_object)
for i in range(key_count):
fcu.keyframe_points.insert(frame=i / key_count, value=0)
@@ -497,7 +510,7 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
fcu = _first_fcurve(key_object)
for i in range(key_count):
fcu.keyframe_points.insert(frame=i / key_count + frame_offset, value=0)

View File

@@ -13,7 +13,7 @@ blender -b --factory-startup --python tests/python/bl_animation_keyframing.py --
"""
def _fcurve_paths_match(fcurves: list, expected_paths: list) -> bool:
def _fcurve_paths_match(fcurves: list[bpy.types.FCurve], expected_paths: list[str]) -> None:
data_paths = list(set([fcurve.data_path for fcurve in fcurves]))
data_paths.sort()
expected_paths.sort()
@@ -22,6 +22,21 @@ def _fcurve_paths_match(fcurves: list, expected_paths: list) -> bool:
f"Expected paths do not match F-Curve paths. Expected: {expected_paths}. F-Curve: {data_paths}")
def _first_channelbag(action: bpy.types.Action) -> bpy.types.ActionChannelbag:
"""Return the first Channelbag of the Action."""
assert isinstance(action, bpy.types.Action), f"Expected Action, got {action!r}"
return action.layers[0].strips[0].channelbags[0]
def _channelbag(animated_id: bpy.types.ID) -> bpy.types.ActionChannelbag:
"""Return the first layer's Channelbag of the animated ID's Action."""
action = animated_id.animation_data.action
action_slot = animated_id.animation_data.action_slot
channelbag = action.layers[0].strips[0].channelbag(action_slot)
assert channelbag is not None
return channelbag
def _get_view3d_context():
ctx = bpy.context.copy()
@@ -86,7 +101,7 @@ def _insert_by_name_test(insert_key: str, expected_paths: list):
keyed_object = _create_animation_object()
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert_by_name(type=insert_key)
_fcurve_paths_match(keyed_object.animation_data.action.fcurves, expected_paths)
_fcurve_paths_match(_channelbag(keyed_object).fcurves, expected_paths)
bpy.data.objects.remove(keyed_object, do_unlink=True)
@@ -95,7 +110,7 @@ def _insert_from_user_preference_test(enabled_user_pref_fields: set, expected_pa
bpy.context.preferences.edit.key_insert_channels = enabled_user_pref_fields
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert()
_fcurve_paths_match(keyed_object.animation_data.action.fcurves, expected_paths)
_fcurve_paths_match(_channelbag(keyed_object).fcurves, expected_paths)
bpy.data.objects.remove(keyed_object, do_unlink=True)
@@ -110,7 +125,7 @@ def _insert_with_keying_set_test(keying_set_name: str, expected_paths: list):
keyed_object = _create_animation_object()
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert()
_fcurve_paths_match(keyed_object.animation_data.action.fcurves, expected_paths)
_fcurve_paths_match(_channelbag(keyed_object).fcurves, expected_paths)
bpy.data.objects.remove(keyed_object, do_unlink=True)
@@ -148,18 +163,20 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert()
channelbag = _channelbag(keyed_object)
# Check the F-Curves paths.
expect_paths = ["location", "location", "location"]
actual_paths = [fcurve.data_path for fcurve in keyed_object.animation_data.action.fcurves]
actual_paths = [fcurve.data_path for fcurve in channelbag.fcurves]
self.assertEqual(actual_paths, expect_paths)
# The actual reason for this test: check that these curves have the right group.
expect_groups = ["Object Transforms"]
actual_groups = [group.name for group in keyed_object.animation_data.action.groups]
actual_groups = [group.name for group in channelbag.groups]
self.assertEqual(actual_groups, expect_groups)
expect_groups = 3 * [keyed_object.animation_data.action.groups[0]]
actual_groups = [fcurve.group for fcurve in keyed_object.animation_data.action.fcurves]
expect_groups = 3 * [channelbag.groups[0]]
actual_groups = [fcurve.group for fcurve in channelbag.fcurves]
self.assertEqual(actual_groups, expect_groups)
def test_insert_custom_properties(self):
@@ -196,7 +213,7 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert()
keyed_rna_paths = [f"[\"{bpy.utils.escape_identifier(path)}\"]" for path in keyed_properties.keys()]
_fcurve_paths_match(keyed_object.animation_data.action.fcurves, keyed_rna_paths)
_fcurve_paths_match(_channelbag(keyed_object).fcurves, keyed_rna_paths)
bpy.data.objects.remove(keyed_object, do_unlink=True)
def test_key_selection_state(self):
@@ -207,7 +224,7 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
bpy.context.scene.frame_set(5)
bpy.ops.anim.keyframe_insert()
for fcurve in keyed_object.animation_data.action.fcurves:
for fcurve in _channelbag(keyed_object).fcurves:
self.assertEqual(len(fcurve.keyframe_points), 2)
self.assertFalse(fcurve.keyframe_points[0].select_control_point)
self.assertTrue(fcurve.keyframe_points[1].select_control_point)
@@ -218,7 +235,7 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
# Test on location, which is a 3-item array, without explicitly passing an array index.
self.assertTrue(curve_object.keyframe_insert('location'))
ob_fcurves = curve_object.animation_data.action.fcurves
ob_fcurves = _channelbag(curve_object).fcurves
self.assertEqual(len(ob_fcurves), 3,
"Keying 'location' without any array index should have created 3 F-Curves")
@@ -249,8 +266,9 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
# Test with property for which Blender knows a group name too ('Object Transforms').
self.assertTrue(curve_object.keyframe_insert('location', group="Téšt"))
fcurves = curve_object.animation_data.action.fcurves
fgroups = curve_object.animation_data.action.groups
channelbag = _channelbag(curve_object)
fcurves = channelbag.fcurves
fgroups = channelbag.groups
self.assertEqual(3 * ['location'], [fcurve.data_path for fcurve in fcurves])
self.assertEqual([0, 1, 2], [fcurve.array_index for fcurve in fcurves])
@@ -271,7 +289,7 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
obj = bpy.context.object
obj.data.attributes.new("test", "FLOAT", "POINT")
self.assertTrue(obj.data.keyframe_insert('attributes["test"].data[0].value'))
fcurves = obj.data.animation_data.action.fcurves
fcurves = _channelbag(obj.data).fcurves
self.assertEqual(len(fcurves), 1)
self.assertEqual(fcurves[0].data_path, 'attributes["test"].data[0].value')
@@ -294,7 +312,7 @@ class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert_by_name(type="BUILTIN_KSI_VisualLoc")
for fcurve in constrained.animation_data.action.fcurves:
for fcurve in _channelbag(constrained).fcurves:
self.assertEqual(fcurve.keyframe_points[0].co.y, t_value)
def test_visual_rotation_keying_set(self):
@@ -310,7 +328,7 @@ class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert_by_name(type="BUILTIN_KSI_VisualRot")
for fcurve in constrained.animation_data.action.fcurves:
for fcurve in _channelbag(constrained).fcurves:
self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, rot_value_rads, places=4)
def test_visual_location_user_pref_override(self):
@@ -327,7 +345,7 @@ class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
for fcurve in constrained.animation_data.action.fcurves:
for fcurve in _channelbag(constrained).fcurves:
self.assertEqual(fcurve.keyframe_points[0].co.y, t_value)
def test_visual_location_user_pref(self):
@@ -344,7 +362,7 @@ class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_insert()
for fcurve in constrained.animation_data.action.fcurves:
for fcurve in _channelbag(constrained).fcurves:
self.assertEqual(fcurve.keyframe_points[0].co.y, t_value)
@@ -391,11 +409,12 @@ class CycleAwareKeyingTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
# Check that only location keys have been created.
_fcurve_paths_match(action.fcurves, ["location"])
channelbag = action.layers[0].strips[0].channelbags[0]
_fcurve_paths_match(channelbag.fcurves, ["location"])
expected_keys = [1.0, 3.0, 5.0, 9.0, 20.0]
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
actual_keys = [key.co.x for key in fcurve.keyframe_points]
self.assertEqual(expected_keys, actual_keys)
@@ -425,7 +444,8 @@ class CycleAwareKeyingTest(AbstractKeyframingTest, unittest.TestCase):
expected_keys = [1.0, 3.0, 5.0, 20.0]
for fcurve in action.fcurves:
channelbag = action.layers[0].strips[0].channelbags[0]
for fcurve in channelbag.fcurves:
actual_keys = [key.co.x for key in fcurve.keyframe_points]
self.assertEqual(expected_keys, actual_keys)
@@ -451,8 +471,8 @@ class AutoKeyframingTest(AbstractKeyframingTest, unittest.TestCase):
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.transform.translate(value=(1, 0, 0))
action = keyed_object.animation_data.action
_fcurve_paths_match(action.fcurves, ["location", "rotation_euler", "scale"])
channelbag = _channelbag(keyed_object)
_fcurve_paths_match(channelbag.fcurves, ["location", "rotation_euler", "scale"])
def test_autokey_bone(self):
armature_obj = _create_armature()
@@ -463,10 +483,10 @@ class AutoKeyframingTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.transform.translate(value=(1, 0, 0))
bpy.ops.object.mode_set(mode='OBJECT')
action = armature_obj.animation_data.action
channelbag = _channelbag(armature_obj)
bone_path = f"pose.bones[\"{_BONE_NAME}\"]"
expected_paths = [f"{bone_path}.location", f"{bone_path}.rotation_euler", f"{bone_path}.scale"]
_fcurve_paths_match(action.fcurves, expected_paths)
_fcurve_paths_match(channelbag.fcurves, expected_paths)
def test_key_selection_state(self):
armature_obj = _create_armature()
@@ -476,8 +496,8 @@ class AutoKeyframingTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.transform.translate(value=(0, 1, 0))
bpy.ops.object.mode_set(mode='OBJECT')
action = armature_obj.animation_data.action
for fcurve in action.fcurves:
channelbag = _channelbag(armature_obj)
for fcurve in channelbag.fcurves:
self.assertEqual(len(fcurve.keyframe_points), 2)
self.assertFalse(fcurve.keyframe_points[0].select_control_point)
self.assertTrue(fcurve.keyframe_points[1].select_control_point)
@@ -507,8 +527,8 @@ class InsertAvailableTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.transform.translate(value=(1, 0, 0))
# Test that no new keyframes have been added.
action = keyed_object.animation_data.action
_fcurve_paths_match(action.fcurves, ["rotation_euler"])
channelbag = _channelbag(keyed_object)
_fcurve_paths_match(channelbag.fcurves, ["rotation_euler"])
with bpy.context.temp_override(**_get_view3d_context()):
bpy.context.scene.frame_set(1)
@@ -516,10 +536,10 @@ class InsertAvailableTest(AbstractKeyframingTest, unittest.TestCase):
bpy.context.scene.frame_set(5)
bpy.ops.transform.translate(value=(1, 0, 0))
action = keyed_object.animation_data.action
_fcurve_paths_match(action.fcurves, ["location", "rotation_euler"])
channelbag = _channelbag(keyed_object)
_fcurve_paths_match(channelbag.fcurves, ["location", "rotation_euler"])
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
# Translating the bone would also add rotation keys as long as "Only Insert Needed" is off.
if "location" in fcurve.data_path or "rotation" in fcurve.data_path:
self.assertEqual(len(fcurve.keyframe_points), 2)
@@ -537,10 +557,10 @@ class InsertAvailableTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.transform.translate(value=(1, 0, 0))
# Test that no new keyframes have been added.
action = armature_obj.animation_data.action
channelbag = _channelbag(armature_obj)
bone_path = f"pose.bones[\"{_BONE_NAME}\"]"
expected_paths = [f"{bone_path}.rotation_euler"]
_fcurve_paths_match(action.fcurves, expected_paths)
_fcurve_paths_match(channelbag.fcurves, expected_paths)
with bpy.context.temp_override(**_get_view3d_context()):
bpy.context.scene.frame_set(1)
@@ -549,9 +569,9 @@ class InsertAvailableTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.transform.translate(value=(1, 0, 0))
expected_paths = [f"{bone_path}.location", f"{bone_path}.rotation_euler"]
_fcurve_paths_match(action.fcurves, expected_paths)
_fcurve_paths_match(channelbag.fcurves, expected_paths)
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
# Translating the bone would also add rotation keys as long as "Only Insert Needed" is off.
if "location" in fcurve.data_path or "rotation" in fcurve.data_path:
self.assertEqual(len(fcurve.keyframe_points), 2)
@@ -572,10 +592,10 @@ class InsertAvailableTest(AbstractKeyframingTest, unittest.TestCase):
bpy.context.scene.frame_set(5)
bpy.ops.anim.keyframe_insert_by_name(type="Available")
action = keyed_object.animation_data.action
_fcurve_paths_match(action.fcurves, ["location"])
channelbag = _channelbag(keyed_object)
_fcurve_paths_match(channelbag.fcurves, ["location"])
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
self.assertEqual(len(fcurve.keyframe_points), 2)
def test_insert_available(self):
@@ -610,8 +630,8 @@ class InsertNeededTest(AbstractKeyframingTest, unittest.TestCase):
bpy.context.scene.frame_set(5)
bpy.ops.transform.translate(value=(1, 0, 0))
action = keyed_object.animation_data.action
_fcurve_paths_match(action.fcurves, ["location"])
channelbag = _channelbag(keyed_object)
_fcurve_paths_match(channelbag.fcurves, ["location"])
# With "Insert Needed" enabled it has to key all location channels first,
# before it can add keys only to the channels where values have actually
@@ -620,9 +640,9 @@ class InsertNeededTest(AbstractKeyframingTest, unittest.TestCase):
"location": (2, 1, 1)
}
self.assertEqual(len(action.fcurves), 3)
self.assertEqual(len(channelbag.fcurves), 3)
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
if fcurve.data_path not in expected_keys:
raise AssertionError(f"Did not expect a key on {fcurve.data_path}")
self.assertEqual(expected_keys[fcurve.data_path][fcurve.array_index], len(fcurve.keyframe_points))
@@ -639,9 +659,9 @@ class InsertNeededTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.object.mode_set(mode='OBJECT')
action = armature_obj.animation_data.action
channelbag = _channelbag(armature_obj)
bone_path = f"pose.bones[\"{_BONE_NAME}\"]"
_fcurve_paths_match(action.fcurves, [f"{bone_path}.location"])
_fcurve_paths_match(channelbag.fcurves, [f"{bone_path}.location"])
# With "Insert Needed" enabled it has to key all location channels first,
# before it can add keys only to the channels where values have actually
@@ -650,9 +670,9 @@ class InsertNeededTest(AbstractKeyframingTest, unittest.TestCase):
f"{bone_path}.location": (2, 1, 1)
}
self.assertEqual(len(action.fcurves), 3)
self.assertEqual(len(channelbag.fcurves), 3)
for fcurve in action.fcurves:
for fcurve in channelbag.fcurves:
if fcurve.data_path not in expected_keys:
raise AssertionError(f"Did not expect a key on {fcurve.data_path}")
self.assertEqual(expected_keys[fcurve.data_path][fcurve.array_index], len(fcurve.keyframe_points))
@@ -668,7 +688,19 @@ def _create_nla_anim_object():
add: 0, 1
base: 0, 1
"""
anim_object = bpy.data.objects.new("anim_object", None)
def _ensure_fcurve(action: bpy.types.Action, *, data_path: str, index: int) -> bpy.types.FCurve:
# Briefly directly assign the Action so that Blender knows what to do.
anim_object.animation_data_create().action = action
try:
fcurve = action.fcurve_ensure_for_datablock(anim_object, data_path=data_path, index=index)
finally:
anim_object.animation_data.action = None
return fcurve
bpy.context.scene.collection.objects.link(anim_object)
bpy.context.view_layer.objects.active = anim_object
anim_object.select_set(True)
@@ -677,7 +709,7 @@ def _create_nla_anim_object():
track = anim_object.animation_data.nla_tracks.new()
track.name = "base"
action_base = bpy.data.actions.new(name="action_base")
fcu = action_base.fcurves.new(data_path="location", index=0)
fcu = _ensure_fcurve(action_base, data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=1).interpolation = 'LINEAR'
track.strips.new("base_strip", 0, action_base)
@@ -686,7 +718,7 @@ def _create_nla_anim_object():
track = anim_object.animation_data.nla_tracks.new()
track.name = "add"
action_add = bpy.data.actions.new(name="action_add")
fcu = action_add.fcurves.new(data_path="location", index=0)
fcu = _ensure_fcurve(action_add, data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=1).interpolation = 'LINEAR'
strip = track.strips.new("add_strip", 0, action_add)
@@ -696,7 +728,7 @@ def _create_nla_anim_object():
track = anim_object.animation_data.nla_tracks.new()
track.name = "top"
action_top = bpy.data.actions.new(name="action_top")
fcu = action_top.fcurves.new(data_path="location", index=0)
fcu = _ensure_fcurve(action_top, data_path="location", index=0)
fcu.keyframe_points.insert(0, value=0).interpolation = 'LINEAR'
fcu.keyframe_points.insert(10, value=0).interpolation = 'LINEAR'
track.strips.new("top_strip", 0, action_top)
@@ -744,12 +776,13 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert()
base_action = bpy.data.actions["action_base"]
channelbag = _first_channelbag(base_action)
# Location X should not have been able to insert a keyframe because the top strip is overriding the result completely,
# making it impossible to calculate which value should be inserted.
self.assertEqual(len(base_action.fcurves.find("location", index=0).keyframe_points), 2)
self.assertEqual(len(channelbag.fcurves.find("location", index=0).keyframe_points), 2)
# Location Y and Z will go through since they have not been defined in the action of the top strip.
self.assertEqual(len(base_action.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(base_action.fcurves.find("location", index=2).keyframe_points), 1)
self.assertEqual(len(channelbag.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(channelbag.fcurves.find("location", index=2).keyframe_points), 1)
def test_insert_additive(self):
nla_anim_object = _create_nla_anim_object()
@@ -778,7 +811,8 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert()
# Check that the expected F-Curves exist.
fcurves_actual = {(f.data_path, f.array_index) for f in base_action.fcurves}
channelbag = _first_channelbag(base_action)
fcurves_actual = {(f.data_path, f.array_index) for f in channelbag.fcurves}
fcurves_expect = {
("location", 0),
("location", 1),
@@ -788,14 +822,14 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
# This should have added keys to Y and Z but not X.
# X already had two keys from the file setup.
self.assertEqual(len(base_action.fcurves.find("location", index=0).keyframe_points), 2)
self.assertEqual(len(base_action.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(base_action.fcurves.find("location", index=2).keyframe_points), 1)
self.assertEqual(len(channelbag.fcurves.find("location", index=0).keyframe_points), 2)
self.assertEqual(len(channelbag.fcurves.find("location", index=1).keyframe_points), 1)
self.assertEqual(len(channelbag.fcurves.find("location", index=2).keyframe_points), 1)
# The keyframe value should not be changed even though the position of the
# object is modified by the additive layer.
self.assertAlmostEqual(nla_anim_object.location.x, 2.0, 8)
fcurve_loc_x = base_action.fcurves.find("location", index=0)
fcurve_loc_x = channelbag.fcurves.find("location", index=0)
self.assertAlmostEqual(fcurve_loc_x.keyframe_points[-1].co[1], 1.0, 8)
@@ -808,8 +842,8 @@ class KeyframeDeleteTest(AbstractKeyframingTest, unittest.TestCase):
bpy.ops.anim.keyframe_insert_by_name(type="Location")
self.assertTrue(armature.animation_data is not None)
self.assertTrue(armature.animation_data.action is not None)
action = armature.animation_data.action
self.assertEqual(len(action.fcurves), 3)
channelbag = _channelbag(armature)
self.assertEqual(len(channelbag.fcurves), 3)
bpy.ops.object.mode_set(mode='POSE')
with bpy.context.temp_override(**_get_view3d_context()):
@@ -817,22 +851,22 @@ class KeyframeDeleteTest(AbstractKeyframingTest, unittest.TestCase):
bpy.context.scene.frame_set(5)
bpy.ops.anim.keyframe_insert_by_name(type="Location")
# This should have added new FCurves for the pose bone.
self.assertEqual(len(action.fcurves), 6)
self.assertEqual(len(channelbag.fcurves), 6)
bpy.ops.anim.keyframe_delete_v3d()
# No Fcurves should yet be deleted.
self.assertEqual(len(action.fcurves), 6)
self.assertEqual(len(action.fcurves[0].keyframe_points), 1)
self.assertEqual(len(channelbag.fcurves), 6)
self.assertEqual(len(channelbag.fcurves[0].keyframe_points), 1)
bpy.context.scene.frame_set(1)
bpy.ops.anim.keyframe_delete_v3d()
# This should leave the object level keyframes of the armature
self.assertEqual(len(action.fcurves), 3)
self.assertEqual(len(channelbag.fcurves), 3)
bpy.ops.object.mode_set(mode='OBJECT')
with bpy.context.temp_override(**_get_view3d_context()):
bpy.ops.anim.keyframe_delete_v3d()
# The last FCurves should be deleted from the object now.
self.assertEqual(len(action.fcurves), 0)
self.assertEqual(len(channelbag.fcurves), 0)
def main():

View File

@@ -35,7 +35,12 @@ class AbstractNlaStripTest(unittest.TestCase):
self.nla_tracks = self.test_object.animation_data.nla_tracks
self.action = bpy.data.actions.new(name="ObjectAction")
x_location_fcurve = self.action.fcurves.new(data_path="location", index=0, action_group="Object Transforms")
slot = self.action.slots.new(self.test_object.id_type, self.test_object.name)
layer = self.action.layers.new("Layer")
strip = layer.strips.new(type="KEYFRAME")
channelbag = strip.channelbags.new(slot)
x_location_fcurve = channelbag.fcurves.new(data_path="location", index=0, group_name="Object Transforms")
for frame in range(1, 5):
x_location_fcurve.keyframe_points.insert(frame, value=frame).interpolation = "CONSTANT"

View File

@@ -52,6 +52,12 @@ def _create_armature():
return armature_obj
def _first_channelbag(action: bpy.types.Action) -> bpy.types.ActionChannelbag:
"""Return the first Channelbag of the Action."""
assert isinstance(action, bpy.types.Action)
return action.layers[0].strips[0].channelbags[0]
class CreateAssetTest(unittest.TestCase):
_library_folder = None
@@ -109,8 +115,9 @@ class CreateAssetTest(unittest.TestCase):
# string_test is not here because it should not be keyed.
}
expected_pose_values.update(_BBONE_VALUES)
self.assertEqual(len(pose_action.fcurves), 26)
for fcurve in pose_action.fcurves:
pose_channelbag = _first_channelbag(pose_action)
self.assertEqual(len(pose_channelbag.fcurves), 26)
for fcurve in pose_channelbag.fcurves:
self.assertTrue(
fcurve.data_path in expected_pose_values,
"Only the selected bone should be in the pose asset")
@@ -158,8 +165,9 @@ class CreateAssetTest(unittest.TestCase):
# string_test is not here because it should not be keyed.
}
expected_pose_values.update(_BBONE_VALUES)
self.assertEqual(len(pose_action.fcurves), 26)
for fcurve in pose_action.fcurves:
pose_channelbag = _first_channelbag(pose_action)
self.assertEqual(len(pose_channelbag.fcurves), 26)
for fcurve in pose_channelbag.fcurves:
self.assertTrue(
fcurve.data_path in expected_pose_values,
"Only the selected bone should be in the pose asset")
@@ -193,8 +201,9 @@ class CreateAssetTest(unittest.TestCase):
# The custom properties are not keyed, thus they should not be in the pose asset.
}
expected_pose_values.update(_BBONE_VALUES)
self.assertEqual(len(pose_action.fcurves), 24)
for fcurve in pose_action.fcurves:
pose_channelbag = _first_channelbag(pose_action)
self.assertEqual(len(pose_channelbag.fcurves), 24)
for fcurve in pose_channelbag.fcurves:
self.assertTrue(
fcurve.data_path in expected_pose_values,
"Only the selected bone should be in the pose asset")

View File

@@ -874,30 +874,31 @@ class USDImportTest(AbstractUSDTest):
8 + i), 1.0, 2, "Unexpected weight for Elbow deform vert")
action = bpy.data.actions['Anim1']
channelbag = action.layers[0].strips[0].channelbags[0]
# Verify the Elbow joint rotation animation.
curve_path = 'pose.bones["Elbow"].rotation_quaternion'
# Quat W
f = action.fcurves.find(curve_path, index=0)
f = channelbag.fcurves.find(curve_path, index=0)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion W curve")
self.assertAlmostEqual(f.evaluate(0), 1.0, 2, "Unexpected value for rotation quaternion W curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.707, 2, "Unexpected value for rotation quaternion W curve at frame 10")
# Quat X
f = action.fcurves.find(curve_path, index=1)
f = channelbag.fcurves.find(curve_path, index=1)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion X curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion X curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.707, 2, "Unexpected value for rotation quaternion X curve at frame 10")
# Quat Y
f = action.fcurves.find(curve_path, index=2)
f = channelbag.fcurves.find(curve_path, index=2)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion Y curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion Y curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Y curve at frame 10")
# Quat Z
f = action.fcurves.find(curve_path, index=3)
f = channelbag.fcurves.find(curve_path, index=3)
self.assertIsNotNone(f, "Couldn't find Elbow rotation quaternion Z curve")
self.assertAlmostEqual(f.evaluate(0), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 0")
self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 10")