diff --git a/tests/python/bl_animation_action.py b/tests/python/bl_animation_action.py index ad5c04e760f..6c88057297b 100644 --- a/tests/python/bl_animation_action.py +++ b/tests/python/bl_animation_action.py @@ -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 diff --git a/tests/python/bl_animation_fcurves.py b/tests/python/bl_animation_fcurves.py index 77f75a5bdc7..af61f9abdd7 100644 --- a/tests/python/bl_animation_fcurves.py +++ b/tests/python/bl_animation_fcurves.py @@ -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) diff --git a/tests/python/bl_animation_keyframing.py b/tests/python/bl_animation_keyframing.py index b02c813b4ce..adee2065b15 100644 --- a/tests/python/bl_animation_keyframing.py +++ b/tests/python/bl_animation_keyframing.py @@ -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(): diff --git a/tests/python/bl_animation_nla_strip.py b/tests/python/bl_animation_nla_strip.py index c725363a859..06fefce5a17 100644 --- a/tests/python/bl_animation_nla_strip.py +++ b/tests/python/bl_animation_nla_strip.py @@ -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" diff --git a/tests/python/bl_pose_assets.py b/tests/python/bl_pose_assets.py index e558a9f36fe..437835f2c25 100644 --- a/tests/python/bl_pose_assets.py +++ b/tests/python/bl_pose_assets.py @@ -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") diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 98a54774640..e1b8203f567 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -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")