Anim: add RNA function Action.fcurve_ensure_for_datablock(...)

Expose the convenience function `blender::animrig::action_fcurve_ensure()`
to RNA as `Action.fcurve_ensure_for_datablock(...)`.

The function requires that the Action is already assigned to the
data-block. It then takes care of slot assignment / creation, as well as
the creation of a layer and a keyframe strip.

This function call:

```python
fcurve = action.fcurve_ensure_for_datablock(ob_cube, "location", index=2)
```

effectively performs this logic:

```python
# Ensure the slot exists and is assigned:
slot = ob_cube.animation_data.slot
if not slot:
    slot = find_slot_for_keying(action)
if not slot:
    slot = action.slots.new(ob_cube.name)
ob_cube.animation_data.slot = slot

# Ensure a layer exists:
if action.layers:
    layer = action.layers[0]
else:
    layer = action.layers.new("Layer")

# Ensure a keyframe strip exists:
if layer.strips:
    strip = layer.strips[0]
else:
    strip = layer.strips.new('KEYFRAME')

# Ensure the channelbag exists:
channelbag = strip.channelbag(slot, ensure=True)

# Ensure the F-Curve exists:
fcurve = channelbag.fcurves.find("location", index=1)
if not fcurve:
    fcurve = channelbag.fcurves.new("location", index=1)
```

Here `find_slot_for_keying()` represents the logic that's also used when
creating keys via the user interface or the `bpy_struct.keyframe_insert()`
function.

Pull Request: https://projects.blender.org/blender/blender/pulls/134686
This commit is contained in:
Sybren A. Stüvel
2025-02-18 16:04:00 +01:00
parent ca388fe313
commit 2393d498cb
9 changed files with 168 additions and 24 deletions
+42
View File
@@ -913,6 +913,48 @@ class SlotHandleLibraryOverridesTest(unittest.TestCase):
-1, "Suzanne should be significantly below Z=0 when animated by the library Action")
class ConvenienceFunctionsTest(unittest.TestCase):
def setUp(self) -> None:
bpy.ops.wm.read_homefile(use_factory_startup=True)
self.action = bpy.data.actions.new('Action')
def test_fcurve_ensure_for_datablock(self) -> None:
# The function should be None-safe.
with self.assertRaises(TypeError):
self.action.fcurve_ensure_for_datablock(None, "location")
self.assertEqual(0, len(self.action.layers))
self.assertEqual(0, len(self.action.slots))
# The function should not work unless the Action is assigned to its target.
ob_cube = bpy.data.objects["Cube"]
with self.assertRaises(RuntimeError):
self.action.fcurve_ensure_for_datablock(ob_cube, "location")
self.assertEqual(0, len(self.action.layers))
self.assertEqual(0, len(self.action.slots))
# The function should not work on empty data paths.
adt = ob_cube.animation_data_create()
adt.action = self.action
with self.assertRaises(RuntimeError):
self.action.fcurve_ensure_for_datablock(ob_cube, "")
self.assertEqual(0, len(self.action.layers))
self.assertEqual(0, len(self.action.slots))
# And finally the happy flow.
fcurve = self.action.fcurve_ensure_for_datablock(ob_cube, "location", index=2)
self.assertEqual(1, len(self.action.layers))
self.assertEqual(1, len(self.action.layers[0].strips))
self.assertEqual('KEYFRAME', self.action.layers[0].strips[0].type)
self.assertEqual(1, len(self.action.slots))
self.assertEqual("location", fcurve.data_path)
self.assertEqual(2, fcurve.array_index)
channelbag = self.action.layers[0].strips[0].channelbags[0]
self.assertEqual(fcurve, channelbag.fcurves[0])
def main():
global args
import argparse