Anim: make it easier to convert from legacy to current Action API

The changes:

1. Add `group_name` to the `channelbag.fcurves.new()` and
   `action.fcurve_ensure_for_datablock()` RNA functions.
2. Add `anim_utils.action_ensure_channelbag_for_slot(action, slot)`.
3. Add `channelbag.fcurves.ensure()` RNA function.

This makes it possible to replace this legacy code:

```py
fcurve = action.fcurves.new("location", index=2, action_group="Name")
```

with this code:

```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.new("location", index=2, group_name="Name")
```

or replace this legacy code:

```py
fcurve = action.fcurves.find("location", index=2, action_group="Name")
if not fcurve:
    fcurve = action.fcurves.new("location", index=2, action_group="Name")
```

with this code:

```py
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.ensure("location", index=2, group_name="Name")
```

Note that the parameter name is different (`action_group` became
`group_name`). This clarifies that this is the name of the group, and
not a reference to the group itself.

This is part of #146586

Pull Request: https://projects.blender.org/blender/blender/pulls/146977
This commit is contained in:
Sybren A. Stüvel
2025-09-30 14:43:56 +02:00
parent 1e8636962e
commit dbcb701eb2
3 changed files with 109 additions and 15 deletions

View File

@@ -96,7 +96,9 @@ def action_get_channelbag_for_slot(action: Action | None, slot: ActionSlot | Non
return None
def _ensure_channelbag_exists(action: Action, slot: ActionSlot) -> ActionChannelbag:
def action_ensure_channelbag_for_slot(action: Action, slot: ActionSlot) -> ActionChannelbag:
"""Ensure a layer and a keyframe strip exists, then ensure that that strip has a channelbag for the slot."""
try:
layer = action.layers[0]
except IndexError:
@@ -732,7 +734,7 @@ class KeyframesCo:
if fcurve is None:
data_path, array_index = fc_key
assert action.is_action_layered
channelbag = _ensure_channelbag_exists(action, action_slot)
channelbag = action_ensure_channelbag_for_slot(action, action_slot)
fcurve = channelbag.fcurves.new(data_path, index=array_index)
keyframe_points = fcurve.keyframe_points

View File

@@ -106,6 +106,7 @@ const EnumPropertyItem default_ActionSlot_target_id_type_items[] = {
# include "UI_interface_icons.hh"
# include "ANIM_action_legacy.hh"
# include "ANIM_fcurve.hh"
# include "ANIM_keyframing.hh"
# include <fmt/format.h>
@@ -641,7 +642,8 @@ static FCurve *rna_Channelbag_fcurve_new(ActionChannelbag *dna_channelbag,
Main *bmain,
ReportList *reports,
const char *data_path,
const int index)
const int index,
const char *group_name)
{
BLI_assert(data_path != nullptr);
if (data_path[0] == '\0') {
@@ -649,8 +651,13 @@ static FCurve *rna_Channelbag_fcurve_new(ActionChannelbag *dna_channelbag,
return nullptr;
}
blender::animrig::FCurveDescriptor descr = {data_path, index};
if (group_name && group_name[0]) {
descr.channel_group = {group_name};
}
animrig::Channelbag &self = dna_channelbag->wrap();
FCurve *fcurve = self.fcurve_create_unique(bmain, {data_path, index});
FCurve *fcurve = self.fcurve_create_unique(bmain, descr);
if (!fcurve) {
BKE_reportf(reports,
RPT_ERROR,
@@ -662,6 +669,29 @@ static FCurve *rna_Channelbag_fcurve_new(ActionChannelbag *dna_channelbag,
return fcurve;
}
static FCurve *rna_Channelbag_fcurve_ensure(ActionChannelbag *dna_channelbag,
Main *bmain,
ReportList *reports,
const char *data_path,
const int index,
const char *group_name)
{
BLI_assert(data_path != nullptr);
if (data_path[0] == '\0') {
BKE_report(reports, RPT_ERROR, "F-Curve data path empty, invalid argument");
return nullptr;
}
blender::animrig::FCurveDescriptor descr = {data_path, index};
if (group_name && group_name[0]) {
descr.channel_group = {group_name};
}
animrig::Channelbag &self = dna_channelbag->wrap();
FCurve &fcurve = self.fcurve_ensure(bmain, descr);
return &fcurve;
}
static FCurve *rna_Channelbag_fcurve_find(ActionChannelbag *dna_channelbag,
ReportList *reports,
const char *data_path,
@@ -1387,7 +1417,8 @@ static FCurve *rna_Action_fcurve_ensure_for_datablock(bAction *_self,
ReportList *reports,
ID *datablock,
const char *data_path,
const int array_index)
const int array_index,
const char *group_name)
{
/* Precondition checks. */
{
@@ -1407,8 +1438,12 @@ static FCurve *rna_Action_fcurve_ensure_for_datablock(bAction *_self,
}
}
FCurve &fcurve = blender::animrig::action_fcurve_ensure(
bmain, *_self, *datablock, {data_path, array_index});
blender::animrig::FCurveDescriptor descriptor = {data_path, array_index};
if (group_name && group_name[0]) {
descriptor.channel_group = std::string(group_name);
}
FCurve &fcurve = blender::animrig::action_fcurve_ensure(bmain, *_self, *datablock, descriptor);
WM_main_add_notifier(NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr);
return &fcurve;
@@ -2501,23 +2536,40 @@ static void rna_def_channelbag_fcurves(BlenderRNA *brna, PropertyRNA *cprop)
srna, "F-Curves", "Collection of F-Curves for a specific action slot, on a specific strip");
/* Channelbag.fcurves.new(...) */
extern FCurve *ActionChannelbagFCurves_new_func(ID * _selfid,
ActionChannelbag * _self,
Main * bmain,
ReportList * reports,
const char *data_path,
int index);
func = RNA_def_function(srna, "new", "rna_Channelbag_fcurve_new");
RNA_def_function_ui_description(func, "Add an F-Curve to the channelbag");
RNA_def_function_flag(func, FUNC_USE_MAIN | FUNC_USE_REPORTS);
parm = RNA_def_string(func, "data_path", nullptr, 0, "Data Path", "F-Curve data path to use");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
RNA_def_int(func, "index", 0, 0, INT_MAX, "Index", "Array index", 0, INT_MAX);
parm = RNA_def_string(
func,
"group_name",
nullptr,
sizeof(bActionGroup::name),
"Group Name",
"Name of the Group for this F-Curve, will be created if it does not exist yet");
parm = RNA_def_pointer(func, "fcurve", "FCurve", "", "Newly created F-Curve");
RNA_def_function_return(func, parm);
/* Channelbag.fcurves.ensure(...) */
func = RNA_def_function(srna, "ensure", "rna_Channelbag_fcurve_ensure");
RNA_def_function_ui_description(
func, "Returns the F-Curve if it already exists, and creates it if necessary");
RNA_def_function_flag(func, FUNC_USE_MAIN | FUNC_USE_REPORTS);
parm = RNA_def_string(func, "data_path", nullptr, 0, "Data Path", "F-Curve data path to use");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
RNA_def_int(func, "index", 0, 0, INT_MAX, "Index", "Array index", 0, INT_MAX);
parm = RNA_def_string(func,
"group_name",
nullptr,
sizeof(bActionGroup::name),
"Group Name",
"Name of the Group for this F-Curve, will be created if it does not exist "
"yet. This parameter is ignored if the F-Curve already exists");
parm = RNA_def_pointer(func, "fcurve", "FCurve", "", "Found or newly created F-Curve");
RNA_def_function_return(func, parm);
/* Channelbag.fcurves.find(...) */
func = RNA_def_function(srna, "find", "rna_Channelbag_fcurve_find");
RNA_def_function_ui_description(
@@ -3080,6 +3132,13 @@ static void rna_def_action(BlenderRNA *brna)
parm = RNA_def_string(func, "data_path", nullptr, 0, "Data Path", "F-Curve data path");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
RNA_def_int(func, "index", 0, 0, INT_MAX, "Index", "Array index", 0, INT_MAX);
RNA_def_string(func,
"group_name",
nullptr,
0,
"Group Name",
"Name of the group for this F-Curve, if any. If the F-Curve already exists, this "
"parameter is ignored");
parm = RNA_def_pointer(func, "fcurve", "FCurve", "", "The found or created F-Curve");
RNA_def_function_return(func, parm);

View File

@@ -607,6 +607,21 @@ class ChannelbagsTest(unittest.TestCase):
self.assertEqual([channelbag], list(self.strip.channelbags))
self.assertEqual(self.slot, channelbag.slot)
def test_ensure_fcurve(self):
channelbag = self.strip.channelbag(self.slot, ensure=True)
self.assertEqual([], channelbag.fcurves[:])
fcurve_1 = channelbag.fcurves.ensure("location", index=2, group_name="Name")
self.assertEqual("location", fcurve_1.data_path)
self.assertEqual(2, fcurve_1.array_index)
self.assertEqual("Name", fcurve_1.group.name)
self.assertIn("Name", channelbag.groups)
self.assertEqual([fcurve_1], channelbag.fcurves[:])
fcurve_2 = channelbag.fcurves.ensure("location", index=2, group_name="Name")
self.assertEqual(fcurve_1, fcurve_2)
self.assertEqual([fcurve_1], channelbag.fcurves[:])
def test_create_remove_fcurves(self):
channelbag = self.strip.channelbags.new(self.slot)
@@ -988,6 +1003,24 @@ class ConvenienceFunctionsTest(unittest.TestCase):
channelbag = self.action.layers[0].strips[0].channelbags[0]
self.assertEqual(fcurve, channelbag.fcurves[0])
def test_fcurve_ensure_for_datablock_group_name(self) -> None:
# Assign the Action to the Cube.
ob_cube = bpy.data.objects["Cube"]
adt = ob_cube.animation_data_create()
adt.action = self.action
with self.assertRaises(IndexError):
self.action.layers[0].strips[0].channelbags[0]
fcurve = self.action.fcurve_ensure_for_datablock(ob_cube, "location", index=2, group_name="grúpa")
channelbag = self.action.layers[0].strips[0].channelbags[0]
self.assertEqual(fcurve, channelbag.fcurves[0])
# Check that the group was also created correctly.
self.assertIn("grúpa", channelbag.groups)
self.assertIn(fcurve, channelbag.groups["grúpa"].channels[:])
def main():
global args