diff --git a/scripts/modules/bpy_extras/anim_utils.py b/scripts/modules/bpy_extras/anim_utils.py index 19f565568dd..e5de54fe94b 100644 --- a/scripts/modules/bpy_extras/anim_utils.py +++ b/scripts/modules/bpy_extras/anim_utils.py @@ -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 diff --git a/source/blender/makesrna/intern/rna_action.cc b/source/blender/makesrna/intern/rna_action.cc index 2eedf711a1e..1c3db9d3892 100644 --- a/source/blender/makesrna/intern/rna_action.cc +++ b/source/blender/makesrna/intern/rna_action.cc @@ -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 @@ -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); diff --git a/tests/python/bl_animation_action.py b/tests/python/bl_animation_action.py index 51adfa21e37..68185fe1332 100644 --- a/tests/python/bl_animation_action.py +++ b/tests/python/bl_animation_action.py @@ -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