Anim: ChannelBag F-Curve management functions (C++/RNA)

Add F-Curve management functions on ChannelBags
(`channelbag.fcurves.xxx`) that are very similar to the legacy Action
functions `Action.fcurves.xxx`.

```python
channelbag = strip.channelbags.new(slot)
fcurve = channelbag.fcurves.new("rotation_quaternion", index=1)
assert channelbag.fcurves[0] == fcurve
channelbag.fcurves.remove(fcurve)
channelbag.fcurves.clear()
```

Pull Request: https://projects.blender.org/blender/blender/pulls/124987
This commit is contained in:
Sybren A. Stüvel
2024-07-18 17:06:12 +02:00
parent 7a260b761d
commit efbdc4e1fa
4 changed files with 240 additions and 17 deletions

View File

@@ -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");

View File

@@ -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,

View File

@@ -520,7 +520,7 @@ static bool rna_KeyframeActionStrip_key_insert(ID *id,
return ok;
}
static std::optional<std::string> rna_ActionChannelBag_path(const PointerRNA *ptr)
static std::optional<std::string> 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 *>(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

View File

@@ -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):