Anim: Remove 'Slotted Actions' experimental flag
This commit takes the 'Slotted Actions' out of the experimental phase. As a result: - All newly created Actions will be slotted Actions. - Legacy Actions loaded from disk will be versioned to slotted Actions. - The new Python API for slots, layers, strips, and channel bags is available. - The legacy Python API for accessing F-Curves and Action Groups is still available, and will operate on the F-Curves/Groups for the first slot only. - Creating an Action by keying (via the UI, operators, or the `rna_struct.keyframe_insert` function) will try and share Actions between related data-blocks. See !126655 for more info about this. - Assigning an Action to a data-block will auto-assign a suitable Action Slot. The logic for this is described below. However, There are cases where this does _not_ automatically assign a slot, and thus the Action will effectively _not_ animate the data-block. Effort has been spent to make Action selection work both reliably for Blender users as well as keep the behaviour the same for Python scripts. Where these two goals did not converge, reliability and understandability for users was prioritised. Auto-selection of the Action Slot upon assigning the Action works as follows. The first rule to find a slot wins. 1. The data-block remembers the slot name that was last assigned. If the newly assigned Action has a slot with that name, it is chosen. 2. If the Action has a slot with the same name as the data-block, it is chosen. 3. If the Action has only one slot, and it has never been assigned to anything, it is chosen. 4. If the Action is assigned to an NLA strip or an Action constraint, and the Action has a single slot, and that slot has a suitable ID type, it is chosen. This last step is what I was referring to with "Where these two goals did not converge, reliability and understandability for users was prioritised." For regular Action assignments (like via the Action selectors in the Properties editor) this rule doesn't apply, even though with legacy Actions the final state ("it is animated by this Action") differs from the final state with slotted Actions ("it has no slot so is not animated"). This is done to support the following workflow: - Create an Action by animating Cube. - In order to animate Suzanne with that same Action, assign the Action to Suzanne. - Start keying Suzanne. This auto-creates and auto-assigns a new slot for Suzanne. If rule 4. above would apply in this case, the 2nd step would automatically select the Cube slot for Suzanne as well, which would immediately overwrite Suzanne's properties with the Cube animation. Technically, this commit: - removes the `WITH_ANIM_BAKLAVA` build flag, - removes the `use_animation_baklava` experimental flag in preferences, - updates the code to properly deal with the fact that empty Actions are now always considered slotted/layered Actions (instead of that relying on the user preference). Note that 'slotted Actions' and 'layered Actions' are the exact same thing, just focusing on different aspects (slot & layers) of the new data model. The "Baklava phase 1" assumptions are still asserted. This means that: - an Action can have zero or one layer, - that layer can have zero or one strip, - that strip must be of type 'keyframe' and be infinite with zero offset. The code to handle legacy Actions is NOT removed in this commit. It will be removed later. For now it's likely better to keep it around as reference to the old behaviour in order to aid in some inevitable bugfixing. Ref: #120406
This commit is contained in:
@@ -400,15 +400,12 @@ add_blender_test(
|
||||
--testdir "${TEST_SRC_DIR}/animation"
|
||||
)
|
||||
|
||||
if(WITH_EXPERIMENTAL_FEATURES)
|
||||
# Only run with Project Baklava enabled.
|
||||
add_blender_test(
|
||||
bl_animation_action
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_action.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/animation"
|
||||
)
|
||||
endif()
|
||||
add_blender_test(
|
||||
bl_animation_action
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_action.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/animation"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_animation_keyframing
|
||||
|
||||
@@ -13,22 +13,11 @@ blender -b --factory-startup --python tests/python/bl_animation_action.py
|
||||
"""
|
||||
|
||||
|
||||
def enable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = True
|
||||
bpy.context.preferences.experimental.use_animation_baklava = True
|
||||
|
||||
|
||||
def disable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = False
|
||||
bpy.context.preferences.experimental.use_animation_baklava = False
|
||||
|
||||
|
||||
class ActionSlotAssignmentTest(unittest.TestCase):
|
||||
"""Test assigning actions & check reference counts."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
||||
enable_experimental_animation_baklava()
|
||||
|
||||
def test_action_assignment(self):
|
||||
# Create new Action.
|
||||
@@ -154,7 +143,6 @@ class LegacyAPIOnLayeredActionTest(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
||||
enable_experimental_animation_baklava()
|
||||
|
||||
self.action = bpy.data.actions.new('LayeredAction')
|
||||
|
||||
@@ -251,35 +239,6 @@ class LegacyAPIOnLayeredActionTest(unittest.TestCase):
|
||||
self.assertNotIn(group, channelbag.groups[:], "A group should be removable via the legacy API")
|
||||
|
||||
|
||||
class TestLegacyLayered(unittest.TestCase):
|
||||
"""Test boundaries between legacy & layered Actions.
|
||||
|
||||
Layered functionality should not be available on legacy actions.
|
||||
"""
|
||||
|
||||
def test_legacy_action(self) -> None:
|
||||
"""Test layered operations on a legacy Action"""
|
||||
|
||||
# Disable Baklava's backward-compatibility with the legacy API to create an actual legacy Action.
|
||||
disable_experimental_animation_baklava()
|
||||
|
||||
act = bpy.data.actions.new('LegacyAction')
|
||||
act.fcurves.new("location", index=0) # Add an FCurve to make this a non-empty legacy Action.
|
||||
self.assertTrue(act.is_action_legacy)
|
||||
self.assertFalse(act.is_action_layered)
|
||||
self.assertFalse(act.is_empty)
|
||||
|
||||
# Adding a layer should be prevented.
|
||||
with self.assertRaises(RuntimeError):
|
||||
act.layers.new("laagje")
|
||||
self.assertSequenceEqual([], act.layers)
|
||||
|
||||
# Adding a slot should be prevented.
|
||||
with self.assertRaises(RuntimeError):
|
||||
act.slots.new()
|
||||
self.assertSequenceEqual([], act.slots)
|
||||
|
||||
|
||||
class ChannelBagsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
anims = bpy.data.actions
|
||||
@@ -435,12 +394,8 @@ class DataPathTest(unittest.TestCase):
|
||||
|
||||
class VersioningTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
enable_experimental_animation_baklava()
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "layered_action_versioning_42.blend"), load_ui=False)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
disable_experimental_animation_baklava()
|
||||
|
||||
def test_nla_conversion(self):
|
||||
nla_object = bpy.data.objects["nla_object"]
|
||||
nla_anim_data = nla_object.animation_data
|
||||
|
||||
@@ -13,16 +13,6 @@ blender -b --factory-startup --python tests/python/bl_animation_keyframing.py --
|
||||
"""
|
||||
|
||||
|
||||
def enable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = True
|
||||
bpy.context.preferences.experimental.use_animation_baklava = True
|
||||
|
||||
|
||||
def disable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = False
|
||||
bpy.context.preferences.experimental.use_animation_baklava = False
|
||||
|
||||
|
||||
def _fcurve_paths_match(fcurves: list, expected_paths: list) -> bool:
|
||||
data_paths = list(set([fcurve.data_path for fcurve in fcurves]))
|
||||
data_paths.sort()
|
||||
@@ -277,20 +267,6 @@ class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
|
||||
self.assertEqual(["Téšt"], [group.name for group in fgroups])
|
||||
|
||||
|
||||
if hasattr(bpy.types, 'ActionSlot'):
|
||||
# This test only makes sense when built with slotted/layered Actions.
|
||||
class LayeredInsertKeyTest(InsertKeyTest):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
enable_experimental_animation_baklava()
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
disable_experimental_animation_baklava()
|
||||
super().tearDownClass()
|
||||
|
||||
|
||||
class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
|
||||
""" Check if visual keying produces the correct keyframe values. """
|
||||
|
||||
@@ -696,6 +672,7 @@ def _create_nla_anim_object():
|
||||
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)
|
||||
assert action_base.is_action_layered
|
||||
|
||||
track = anim_object.animation_data.nla_tracks.new()
|
||||
track.name = "add"
|
||||
@@ -705,6 +682,7 @@ def _create_nla_anim_object():
|
||||
fcu.keyframe_points.insert(10, value=1).interpolation = 'LINEAR'
|
||||
strip = track.strips.new("add_strip", 0, action_add)
|
||||
strip.blend_type = "ADD"
|
||||
assert action_add.is_action_layered
|
||||
|
||||
track = anim_object.animation_data.nla_tracks.new()
|
||||
track.name = "top"
|
||||
@@ -713,6 +691,7 @@ def _create_nla_anim_object():
|
||||
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)
|
||||
assert action_top.is_action_layered
|
||||
|
||||
return anim_object
|
||||
|
||||
@@ -734,6 +713,11 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
|
||||
area.type = "NLA_EDITOR"
|
||||
break
|
||||
|
||||
# Deselect the default cube, because the NLA tests work on a specific
|
||||
# object created for that test. Operators that work on all selected
|
||||
# objects shouldn't work on anything else but that object.
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
def test_insert_failure(self):
|
||||
# If the topmost track is set to "REPLACE" the system will fail
|
||||
# when trying to insert keys into a layer beneath.
|
||||
@@ -762,6 +746,9 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
|
||||
nla_anim_object = _create_nla_anim_object()
|
||||
tracks = nla_anim_object.animation_data.nla_tracks
|
||||
|
||||
self.assertEqual(nla_anim_object, bpy.context.active_object)
|
||||
self.assertEqual(None, nla_anim_object.animation_data.action)
|
||||
|
||||
# This leaves the additive track as the topmost track with influence
|
||||
tracks["top"].mute = True
|
||||
|
||||
@@ -771,12 +758,25 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase):
|
||||
tracks["base"].strips[0].select = True
|
||||
bpy.ops.nla.tweakmode_enter(use_upper_stack_evaluation=True)
|
||||
|
||||
base_action = bpy.data.actions["action_base"]
|
||||
|
||||
# Verify that tweak mode has switched to the correct Action.
|
||||
self.assertEqual(base_action, nla_anim_object.animation_data.action)
|
||||
|
||||
# Inserting over the existing keyframe.
|
||||
bpy.context.scene.frame_set(10)
|
||||
with bpy.context.temp_override(**_get_view3d_context()):
|
||||
bpy.ops.anim.keyframe_insert()
|
||||
|
||||
base_action = bpy.data.actions["action_base"]
|
||||
# Check that the expected F-Curves exist.
|
||||
fcurves_actual = {(f.data_path, f.array_index) for f in base_action.fcurves}
|
||||
fcurves_expect = {
|
||||
("location", 0),
|
||||
("location", 1),
|
||||
("location", 2),
|
||||
}
|
||||
self.assertEqual(fcurves_actual, fcurves_expect)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -13,16 +13,6 @@ import sys
|
||||
import unittest
|
||||
|
||||
|
||||
def enable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = True
|
||||
bpy.context.preferences.experimental.use_animation_baklava = True
|
||||
|
||||
|
||||
def disable_experimental_animation_baklava():
|
||||
bpy.context.preferences.view.show_developer_ui = False
|
||||
bpy.context.preferences.experimental.use_animation_baklava = False
|
||||
|
||||
|
||||
class AbstractNlaStripTest(unittest.TestCase):
|
||||
""" Sets up a series of strips in one NLA track. """
|
||||
|
||||
@@ -124,15 +114,6 @@ class NlaStripBoundaryTest(AbstractNlaStripTest):
|
||||
|
||||
|
||||
class NLAStripActionSlotSelectionTest(AbstractNlaStripTest):
|
||||
|
||||
def setUp(self):
|
||||
enable_experimental_animation_baklava()
|
||||
return super().setUp()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
disable_experimental_animation_baklava()
|
||||
return super().tearDown()
|
||||
|
||||
def test_two_strips_for_same_action(self):
|
||||
action = bpy.data.actions.new("StripAction")
|
||||
action.slots.new()
|
||||
|
||||
@@ -443,16 +443,6 @@ class CopyTransformsTest(AbstractConstraintTests):
|
||||
class ActionConstraintTest(AbstractConstraintTests):
|
||||
layer_collection = "Action"
|
||||
|
||||
def setUp(self):
|
||||
bpy.context.preferences.view.show_developer_ui = True
|
||||
bpy.context.preferences.experimental.use_animation_baklava = True
|
||||
return super().setUp()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
bpy.context.preferences.view.show_developer_ui = False
|
||||
bpy.context.preferences.experimental.use_animation_baklava = False
|
||||
return super().tearDown()
|
||||
|
||||
def constraint(self) -> Constraint:
|
||||
owner = bpy.context.scene.objects["Action.owner"]
|
||||
constraint = owner.constraints["Action"]
|
||||
|
||||
Reference in New Issue
Block a user