diff --git a/source/blender/animrig/ANIM_action.hh b/source/blender/animrig/ANIM_action.hh index 27cd1d91896..3b6889e0e02 100644 --- a/source/blender/animrig/ANIM_action.hh +++ b/source/blender/animrig/ANIM_action.hh @@ -715,6 +715,36 @@ class ChannelBag : public ::ActionChannelBag { * exist. */ FCurve &fcurve_ensure(FCurveDescriptor fcurve_descriptor); + + /** + * Create an F-Curve, but only if it doesn't exist yet in this ChannelBag. + * + * \return the F-Curve it it was created, or nullptr if it already existed. + */ + FCurve *fcurve_create_unique(FCurveDescriptor fcurve_descriptor); + + /** + * Remove an F-Curve from the ChannelBag. + * + * After this call, if the F-Curve was found, the reference will no longer be + * valid, as the curve will have been freed. + * + * \return true when the F-Curve was found & removed, false if it wasn't found. + */ + bool fcurve_remove(FCurve &fcurve_to_remove); + + /** + * Remove all F-Curves from this ChannelBag. + */ + void fcurves_clear(); + + protected: + /** + * Create an F-Curve. + * + * Assumes that there is no such F-Curve yet on this ChannelBag. + */ + FCurve &fcurve_create(FCurveDescriptor fcurve_descriptor); }; static_assert(sizeof(ChannelBag) == sizeof(::ActionChannelBag), "DNA struct and its C++ wrapper must have the same size"); diff --git a/source/blender/animrig/intern/action.cc b/source/blender/animrig/intern/action.cc index 8fbf48bea26..3fd277b1388 100644 --- a/source/blender/animrig/intern/action.cc +++ b/source/blender/animrig/intern/action.cc @@ -1129,7 +1129,19 @@ FCurve &ChannelBag::fcurve_ensure(const FCurveDescriptor fcurve_descriptor) if (FCurve *existing_fcurve = this->fcurve_find(fcurve_descriptor)) { return *existing_fcurve; } + return this->fcurve_create(fcurve_descriptor); +} +FCurve *ChannelBag::fcurve_create_unique(FCurveDescriptor fcurve_descriptor) +{ + if (this->fcurve_find(fcurve_descriptor)) { + return nullptr; + } + return &this->fcurve_create(fcurve_descriptor); +} + +FCurve &ChannelBag::fcurve_create(FCurveDescriptor fcurve_descriptor) +{ FCurve *new_fcurve = create_fcurve_for_channel(fcurve_descriptor); if (this->fcurve_array_num == 0) { @@ -1140,6 +1152,29 @@ FCurve &ChannelBag::fcurve_ensure(const FCurveDescriptor fcurve_descriptor) return *new_fcurve; } +static void fcurve_ptr_destructor(FCurve **fcurve_ptr) +{ + BKE_fcurve_free(*fcurve_ptr); +}; + +bool ChannelBag::fcurve_remove(FCurve &fcurve_to_remove) +{ + const int64_t fcurve_index = this->fcurves().as_span().first_index_try(&fcurve_to_remove); + if (fcurve_index < 0) { + return false; + } + + dna::array::remove_index( + &this->fcurve_array, &this->fcurve_array_num, nullptr, fcurve_index, fcurve_ptr_destructor); + + return true; +} + +void ChannelBag::fcurves_clear() +{ + dna::array::clear(&this->fcurve_array, &this->fcurve_array_num, nullptr, fcurve_ptr_destructor); +} + SingleKeyingResult KeyframeStrip::keyframe_insert(const Slot &slot, const FCurveDescriptor fcurve_descriptor, const float2 time_value, diff --git a/source/blender/makesrna/intern/rna_action.cc b/source/blender/makesrna/intern/rna_action.cc index f19b095ee92..7fde1f73c70 100644 --- a/source/blender/makesrna/intern/rna_action.cc +++ b/source/blender/makesrna/intern/rna_action.cc @@ -520,7 +520,7 @@ static bool rna_KeyframeActionStrip_key_insert(ID *id, return ok; } -static std::optional rna_ActionChannelBag_path(const PointerRNA *ptr) +static std::optional rna_ChannelBag_path(const PointerRNA *ptr) { animrig::Action &action = rna_action(ptr); animrig::ChannelBag &cbag_to_find = rna_data_channelbag(ptr); @@ -561,6 +561,71 @@ static int rna_iterator_ChannelBag_fcurves_length(PointerRNA *ptr) return bag.fcurves().size(); } +static FCurve *rna_ChannelBag_fcurve_new(ActionChannelBag *dna_channelbag, + ReportList *reports, + const char *data_path, + const int index) +{ + BLI_assert(data_path != nullptr); + if (data_path[0] == '\0') { + BKE_report(reports, RPT_ERROR, "F-Curve data path empty, invalid argument"); + return nullptr; + } + + animrig::ChannelBag &self = dna_channelbag->wrap(); + FCurve *fcurve = self.fcurve_create_unique({data_path, index}); + if (!fcurve) { + BKE_reportf(reports, + RPT_ERROR, + "F-Curve '%s[%d]' already exists in this channelbag", + data_path, + index); + return nullptr; + } + return fcurve; +} + +static FCurve *rna_ChannelBag_fcurve_find(ActionChannelBag *dna_channelbag, + ReportList *reports, + const char *data_path, + const int index) +{ + if (data_path[0] == '\0') { + BKE_report(reports, RPT_ERROR, "F-Curve data path empty, invalid argument"); + return nullptr; + } + + animrig::ChannelBag &self = dna_channelbag->wrap(); + return self.fcurve_find({data_path, index}); +} + +static void rna_ChannelBag_fcurve_remove(ID *dna_action_id, + ActionChannelBag *dna_channelbag, + bContext *C, + ReportList *reports, + PointerRNA *fcurve_ptr) +{ + animrig::ChannelBag &self = dna_channelbag->wrap(); + FCurve *fcurve = static_cast(fcurve_ptr->data); + + if (!self.fcurve_remove(*fcurve)) { + BKE_reportf(reports, RPT_ERROR, "F-Curve not found"); + return; + } + + DEG_id_tag_update(dna_action_id, ID_RECALC_ANIMATION_NO_FLUSH); + WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr); +} + +static void rna_ChannelBag_fcurve_clear(ID *dna_action_id, + ActionChannelBag *dna_channelbag, + bContext *C) +{ + dna_channelbag->wrap().fcurves_clear(); + DEG_id_tag_update(dna_action_id, ID_RECALC_ANIMATION_NO_FLUSH); + WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr); +} + static ActionChannelBag *rna_KeyframeActionStrip_channels(KeyframeActionStrip *self, const animrig::slot_handle_t slot_handle) { @@ -1667,14 +1732,63 @@ static void rna_def_action_strip(BlenderRNA *brna) rna_def_action_keyframe_strip(brna); } -static void rna_def_channelbag_for_slot_fcurves(BlenderRNA *brna, PropertyRNA *cprop) +static void rna_def_channelbag_fcurves(BlenderRNA *brna, PropertyRNA *cprop) { StructRNA *srna; + FunctionRNA *func; + PropertyRNA *parm; + RNA_def_property_srna(cprop, "ActionChannelBagFCurves"); srna = RNA_def_struct(brna, "ActionChannelBagFCurves", nullptr); - RNA_def_struct_sdna(srna, "bActionChannelBag"); - RNA_def_struct_ui_text(srna, "F-Curves", "Collection of F-Curves for a specific action slot"); + RNA_def_struct_sdna(srna, "ActionChannelBag"); + RNA_def_struct_ui_text( + srna, "F-Curves", "Collection of F-Curves for a specific action slot, on a specific strip"); + + /* ChannelBag.fcurves.new(...) */ + extern struct FCurve *ActionChannelBagFCurves_new_func(struct ID * _selfid, + struct 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_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_pointer(func, "fcurve", "FCurve", "", "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( + func, + "Find an F-Curve. Note that this function performs a linear scan " + "of all F-Curves in the channelbag."); + RNA_def_function_flag(func, FUNC_USE_REPORTS); + 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); + parm = RNA_def_pointer( + func, "fcurve", "FCurve", "", "The found F-Curve, or None if it doesn't exist"); + RNA_def_function_return(func, parm); + + /* ChannelBag.fcurves.remove(...) */ + func = RNA_def_function(srna, "remove", "rna_ChannelBag_fcurve_remove"); + RNA_def_function_ui_description(func, "Remove F-Curve"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_SELF_ID | FUNC_USE_REPORTS); + parm = RNA_def_pointer(func, "fcurve", "FCurve", "", "F-Curve to remove"); + RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED | PARM_RNAPTR); + RNA_def_parameter_clear_flags(parm, PROP_THICK_WRAP, ParameterFlag(0)); + + /* ChannelBag.fcurves.clear() */ + func = RNA_def_function(srna, "clear", "rna_ChannelBag_fcurve_clear"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_SELF_ID); + RNA_def_function_ui_description(func, "Remove all F-Curves from this channelbag"); } static void rna_def_action_channelbag(BlenderRNA *brna) @@ -1687,7 +1801,7 @@ static void rna_def_action_channelbag(BlenderRNA *brna) srna, "Animation Channel Bag", "Collection of animation channels, typically associated with an action slot"); - RNA_def_struct_path_func(srna, "rna_ActionChannelBag_path"); + RNA_def_struct_path_func(srna, "rna_ChannelBag_path"); prop = RNA_def_property(srna, "slot_handle", PROP_INT, PROP_NONE); RNA_def_property_clear_flag(prop, PROP_EDITABLE); @@ -1704,7 +1818,7 @@ static void rna_def_action_channelbag(BlenderRNA *brna) nullptr); RNA_def_property_struct_type(prop, "FCurve"); RNA_def_property_ui_text(prop, "F-Curves", "The individual F-Curves that animate the slot"); - rna_def_channelbag_for_slot_fcurves(brna, prop); + rna_def_channelbag_fcurves(brna, prop); } # endif // WITH_ANIM_BAKLAVA diff --git a/tests/python/bl_animation_action.py b/tests/python/bl_animation_action.py index e63282db286..f5ca106dbe7 100644 --- a/tests/python/bl_animation_action.py +++ b/tests/python/bl_animation_action.py @@ -170,22 +170,66 @@ class ChannelBagsTest(unittest.TestCase): while anims: anims.remove(anims[0]) + self.action = bpy.data.actions.new('TestAction') + + self.slot = self.action.slots.new() + self.slot.name = 'OBTest' + + self.layer = self.action.layers.new(name="Layer") + self.strip = self.layer.strips.new(type='KEYFRAME') + def test_create_remove_channelbag(self): - action = bpy.data.actions.new('TestAction') + channelbag = self.strip.channelbags.new(self.slot) - slot = action.slots.new() - slot.name = 'OBTest' - - layer = action.layers.new(name="Layer") - strip = layer.strips.new(type='KEYFRAME') - channelbag = strip.channelbags.new(slot) - - strip.key_insert(slot, "location", 1, 47.0, 327.0) + self.strip.key_insert(self.slot, "location", 1, 47.0, 327.0) self.assertEqual("location", channelbag.fcurves[0].data_path, "Keys for the channelbag's slot should go into the channelbag") - strip.channelbags.remove(channelbag) - self.assertEqual([], list(strip.channelbags)) + self.strip.channelbags.remove(channelbag) + self.assertEqual([], list(self.strip.channelbags)) + + def test_create_remove_fcurves(self): + channelbag = self.strip.channelbags.new(self.slot) + + # Creating an F-Curve should work. + fcurve = channelbag.fcurves.new('location', index=1) + self.assertIsNotNone(fcurve) + self.assertEquals(fcurve.data_path, 'location') + self.assertEquals(fcurve.array_index, 1) + self.assertEquals([fcurve], channelbag.fcurves[:]) + + # Empty data paths should not be accepted. + with self.assertRaises(RuntimeError): + channelbag.fcurves.new('', index=1) + self.assertEquals([fcurve], channelbag.fcurves[:]) + + # Creating an F-Curve twice should fail: + with self.assertRaises(RuntimeError): + channelbag.fcurves.new('location', index=1) + self.assertEquals([fcurve], channelbag.fcurves[:]) + + # Removing an unrelated F-Curve should fail, even when an F-Curve with + # the same RNA path and array index exists. + other_slot = self.action.slots.new() + other_cbag = self.strip.channelbags.new(other_slot) + other_fcurve = other_cbag.fcurves.new('location', index=1) + with self.assertRaises(RuntimeError): + channelbag.fcurves.remove(other_fcurve) + self.assertEquals([fcurve], channelbag.fcurves[:]) + + # Removing an existing F-Curve should work: + channelbag.fcurves.remove(fcurve) + self.assertEquals([], channelbag.fcurves[:]) + + def test_fcurves_clear(self): + channelbag = self.strip.channelbags.new(self.slot) + + for index in range(4): + channelbag.fcurves.new('rotation_quaternion', index=index) + + self.assertEquals(4, len(channelbag.fcurves)) + channelbag.fcurves.clear() + self.assertEquals([], channelbag.fcurves[:]) class DataPathTest(unittest.TestCase):