blender/tests/python/bl_animation_nla_strip.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

159 lines
6.5 KiB
Python
Raw Permalink Normal View History

# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Tests the evaluation of NLA strips based on their properties and placement on NLA tracks.
blender -b --factory-startup --python tests/python/bl_animation_nla_strip.py
"""
import bpy
import sys
import unittest
class AbstractNlaStripTest(unittest.TestCase):
""" Sets up a series of strips in one NLA track. """
test_object: bpy.types.Object = None
""" Object whose X Location is animated to check strip evaluation. """
nla_tracks: bpy.types.NlaTracks = None
""" NLA tracks of the test object, which are cleared after each test case. """
action: bpy.types.Action = None
""" Action with X Location keyed on frames 1 to 4 with the same value as the frame, with constant interpolation. """
def setUp(self):
bpy.ops.wm.read_factory_settings(use_empty=True)
self.test_object = bpy.data.objects.new(name="Object", object_data=bpy.data.meshes.new("Mesh"))
bpy.context.collection.objects.link(self.test_object)
self.test_object.animation_data_create()
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")
for frame in range(1, 5):
x_location_fcurve.keyframe_points.insert(frame, value=frame).interpolation = "CONSTANT"
def add_strip_no_extrapolation(self, nla_track: bpy.types.NlaTrack, start: int) -> bpy.types.NlaStrip:
""" Places a new strip with the test action on the given track, setting extrapolation to nothing. """
strip = nla_track.strips.new("ObjectAction", start, self.action)
strip.extrapolation = "NOTHING"
return strip
def assertFrameValue(self, frame: float, expected_value: float):
""" Checks the evaluated X Location at the given frame. """
int_frame, subframe = divmod(frame, 1)
bpy.context.scene.frame_set(frame=int(int_frame), subframe=subframe)
self.assertEqual(expected_value, self.test_object.evaluated_get(
bpy.context.evaluated_depsgraph_get()
).matrix_world.translation[0])
class NlaStripSingleTest(AbstractNlaStripTest):
""" Tests the inner values as well as the boundaries of one strip on one track. """
def test_extrapolation_nothing(self):
""" Tests one strip with no extrapolation. """
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
self.assertFrameValue(0.9, 0.0)
self.assertFrameValue(1.0, 1.0)
self.assertFrameValue(1.1, 1.0)
self.assertFrameValue(3.9, 3.0)
self.assertFrameValue(4.0, 4.0)
self.assertFrameValue(4.1, 0.0)
class NlaStripBoundaryTest(AbstractNlaStripTest):
""" Tests two strips, the second one starting when the first one ends. """
# Incorrectly, the first strip is currently evaluated at the boundary between two adjacent strips (see #113487).
@unittest.expectedFailure
def test_adjacent(self):
""" The second strip should be evaluated at the boundary between two adjacent strips. """
nla_track = self.nla_tracks.new()
self.add_strip_no_extrapolation(nla_track, 1)
self.add_strip_no_extrapolation(nla_track, 4)
self.assertFrameValue(3.9, 3.0)
self.assertFrameValue(4.0, 1.0)
self.assertFrameValue(4.1, 1.0)
def test_adjacent_muted(self):
""" The first strip should be evaluated at the boundary if it is adjacent to a muted strip. """
nla_track = self.nla_tracks.new()
self.add_strip_no_extrapolation(nla_track, 1)
self.add_strip_no_extrapolation(nla_track, 4).mute = True
self.assertFrameValue(3.9, 3.0)
self.assertFrameValue(4.0, 4.0)
self.assertFrameValue(4.1, 0.0)
def test_first_above_second(self):
""" The first strip should be evaluated at the boundary, when followed by another strip on a track below. """
self.add_strip_no_extrapolation(self.nla_tracks.new(), 4)
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
self.assertFrameValue(3.9, 3.0)
self.assertFrameValue(4.0, 4.0)
self.assertFrameValue(4.1, 1.0)
def test_second_above_first(self):
""" The second strip should be evaluated at the boundary, when preceded by another strip on a track below. """
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
self.add_strip_no_extrapolation(self.nla_tracks.new(), 4)
self.assertFrameValue(3.9, 3.0)
self.assertFrameValue(4.0, 1.0)
self.assertFrameValue(4.1, 1.0)
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
class NLAStripActionSlotSelectionTest(AbstractNlaStripTest):
def test_two_strips_for_same_action(self):
action = bpy.data.actions.new("StripAction")
Anim: change parameters of `slots.new()` RNA function `Action.slots.new()` in the Python API previously took either an ID or nothing as a parameter. In the former case it would create a slot with the appropriate `id_root` and name for that ID. In the latter case it would create a default slot with an unspecified `id_root` and default name. This had several issues: 1. You couldn't create a slot with a specific `id_root` without already having an ID of that type. In theory this isn't a problem, but in practice in larger scripts/addons you don't necessarily have such an ID on hand at the call site. 2. You couldn't directly create a slot with a desired name without an existing ID with that name. This isn't so important, since you can always just set the name afterwards. But it's a bit annoying. 3. Most other `new()` APIs in Blender *require* you to specify the name of the item being created. So calling this with no parameters was violating that norm. 4. Ideally, we want to eliminate unspecified `id_root`s, since they cause other weirdness in the API such as slot identifiers changing upon slot assignment. To resolve these issues, and just generally to make the API more straightforward, this PR changes `slots.new()` to take two required parameters: an ID type and a name. For example: `slots.new(id_type='CAMERA', name="My Camera Data Slot")`. This fully specifies everything needed for the slot identifier upon creation, and doesn't require any outside data items to create a slot with the desired type and name. In the future if we decide we still want a `for_id`-style slot creation API, we can reintroduce it as a separate function. Ref: #130892 Pull Request: https://projects.blender.org/blender/blender/pulls/130970
2024-12-02 17:04:37 +01:00
action.slots.new('OBJECT', "Slot")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
self.assertTrue(action.is_action_layered)
self.assertEqual(1, len(action.slots))
track = self.nla_tracks.new()
strip1 = track.strips.new("name", 1, action)
self.assertEqual(action.slots[0], strip1.action_slot)
self.assertEqual('OBJECT', action.slots[0].target_id_type, "Slot should have been rooted to object")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
strip2 = track.strips.new("name", 10, action)
self.assertEqual(action.slots[0], strip2.action_slot)
def test_switch_action_via_assignment(self):
action1 = bpy.data.actions.new("StripAction 1")
Anim: change parameters of `slots.new()` RNA function `Action.slots.new()` in the Python API previously took either an ID or nothing as a parameter. In the former case it would create a slot with the appropriate `id_root` and name for that ID. In the latter case it would create a default slot with an unspecified `id_root` and default name. This had several issues: 1. You couldn't create a slot with a specific `id_root` without already having an ID of that type. In theory this isn't a problem, but in practice in larger scripts/addons you don't necessarily have such an ID on hand at the call site. 2. You couldn't directly create a slot with a desired name without an existing ID with that name. This isn't so important, since you can always just set the name afterwards. But it's a bit annoying. 3. Most other `new()` APIs in Blender *require* you to specify the name of the item being created. So calling this with no parameters was violating that norm. 4. Ideally, we want to eliminate unspecified `id_root`s, since they cause other weirdness in the API such as slot identifiers changing upon slot assignment. To resolve these issues, and just generally to make the API more straightforward, this PR changes `slots.new()` to take two required parameters: an ID type and a name. For example: `slots.new(id_type='CAMERA', name="My Camera Data Slot")`. This fully specifies everything needed for the slot identifier upon creation, and doesn't require any outside data items to create a slot with the desired type and name. In the future if we decide we still want a `for_id`-style slot creation API, we can reintroduce it as a separate function. Ref: #130892 Pull Request: https://projects.blender.org/blender/blender/pulls/130970
2024-12-02 17:04:37 +01:00
action1.slots.new('OBJECT', "Slot")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
self.assertTrue(action1.is_action_layered)
self.assertEqual(1, len(action1.slots))
action2 = bpy.data.actions.new("StripAction 2")
Anim: change parameters of `slots.new()` RNA function `Action.slots.new()` in the Python API previously took either an ID or nothing as a parameter. In the former case it would create a slot with the appropriate `id_root` and name for that ID. In the latter case it would create a default slot with an unspecified `id_root` and default name. This had several issues: 1. You couldn't create a slot with a specific `id_root` without already having an ID of that type. In theory this isn't a problem, but in practice in larger scripts/addons you don't necessarily have such an ID on hand at the call site. 2. You couldn't directly create a slot with a desired name without an existing ID with that name. This isn't so important, since you can always just set the name afterwards. But it's a bit annoying. 3. Most other `new()` APIs in Blender *require* you to specify the name of the item being created. So calling this with no parameters was violating that norm. 4. Ideally, we want to eliminate unspecified `id_root`s, since they cause other weirdness in the API such as slot identifiers changing upon slot assignment. To resolve these issues, and just generally to make the API more straightforward, this PR changes `slots.new()` to take two required parameters: an ID type and a name. For example: `slots.new(id_type='CAMERA', name="My Camera Data Slot")`. This fully specifies everything needed for the slot identifier upon creation, and doesn't require any outside data items to create a slot with the desired type and name. In the future if we decide we still want a `for_id`-style slot creation API, we can reintroduce it as a separate function. Ref: #130892 Pull Request: https://projects.blender.org/blender/blender/pulls/130970
2024-12-02 17:04:37 +01:00
action2.slots.new('OBJECT', "Slot")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
self.assertTrue(action2.is_action_layered)
self.assertEqual(1, len(action2.slots))
track = self.nla_tracks.new()
strip = track.strips.new("name", 1, action1)
self.assertEqual(action1.slots[0], strip.action_slot)
self.assertEqual('OBJECT', action1.slots[0].target_id_type,
"Slot of Action 1 should have been rooted to object")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
strip.action = action2
self.assertEqual(action2.slots[0], strip.action_slot)
self.assertEqual('OBJECT', action2.slots[0].target_id_type,
"Slot of Action 2 should have been rooted to object")
Anim: Fix NLA Strip Action assignment When creating a new NLA strip for an action, as well as when setting `strip.action` via RNA, use the generic action-assignment code. This ensures that the slot selection follows the same logic as other Action assignments. If the generic slot selection doesn't find a suitable slot, and there is a single slot on that Action of a suitable ID type, always assign it. This is to support the following scenario: - Python script creates an Action and adds F-Curves via the legacy API. - This creates a slot 'XXSlot'. - The script creates multiple NLA strips for that Action. - The desired result is that these strips get the same Slot assigned as well. The generic code doesn't work for this, because: - The first strip assignment would see the slot `XXSlot` (`XX` indicating "not bound to any ID type yet"). Because that slot has never been used, it will be assigned (which is good). This assignment would change its name to, for example, `OBSlot`. - The second strip assignment would not see a 'virgin' slot, and thus not auto-select `OBSlot`. This behaviour makes sense when assigning Actions in the Action editor (assigning an Action that already animates 'Cube' to 'Suzanne' should not assign the 'OBCube' slot to Suzanne), but for the NLA I feel that it could be a bit more 'enthousiastic' in auto-picking a slot to support the above case. This is preparation for the removal of the 'Slotted Actions' experimental flag, and getting the new code to run as compatibly as possible with the legacy code. The return value of `animrig::nla::assign_action()` has changed a bit. It used to indicate whether a slot was auto-selected; it now indicates whether the Action assignment was successful. Whether a slot was assigned or not can be seen at `strip.action_slot`. Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 10:38:24 +02:00
if __name__ == "__main__":
# Drop all arguments before "--", or everything if the delimiter is absent. Keep the executable path.
unittest.main(argv=sys.argv[:1] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []))