Anim: add Action::slot_remove() method to remove an action slot

Add a method to remove a slot from an Action. This is also exposed as
`Action.slots.remove(slot)` to RNA/Python.

Removing a slot deletes all its associated animation data in the Action.
It also un-assigns the slot from any ID that was animated by it. The
Action reference is left untouched.

Pull Request: https://projects.blender.org/blender/blender/pulls/127112
This commit is contained in:
Sybren A. Stüvel
2024-09-03 15:48:00 +02:00
parent 4221f827cf
commit f7d4a517e3
4 changed files with 234 additions and 1 deletions

View File

@@ -199,6 +199,15 @@ class Action : public ::bAction {
*/
Slot &slot_add_for_id(const ID &animated_id);
/**
* Remove a slot, and ALL animation data that belongs to it.
*
* After this call, the reference is no longer valid as the slot will have been freed.
*
* \return true when the layer was found & removed, false if it wasn't found.
*/
bool slot_remove(Slot &slot_to_remove);
/**
* Set the active Slot, ensuring only one Slot is flagged as the Active one.
*
@@ -276,6 +285,9 @@ class Action : public ::bAction {
/** Return the layer's index, or -1 if not found in this Action. */
int64_t find_layer_index(const Layer &layer) const;
/** Return the slot's index, or -1 if not found in this Action. */
int64_t find_slot_index(const Slot &slot) const;
private:
Slot &slot_allocate();
@@ -368,6 +380,13 @@ class Strip : public ::ActionStrip {
* (negative for frame_start, positive for frame_end) are supported.
*/
void resize(float frame_start, float frame_end);
/**
* Remove all data belonging to the given slot.
*
* This is typically only called from Layer::slot_data_remove().
*/
void slot_data_remove(slot_handle_t slot_handle);
};
static_assert(sizeof(Strip) == sizeof(::ActionStrip),
"DNA struct and its C++ wrapper must have the same size");
@@ -455,6 +474,13 @@ class Layer : public ::ActionLayer {
*/
bool strip_remove(Strip &strip);
/**
* Remove all data belonging to the given slot.
*
* This is typically only called from Action::slot_remove().
*/
void slot_data_remove(slot_handle_t slot_handle);
protected:
/** Return the strip's index, or -1 if not found in this layer. */
int64_t find_strip_index(const Strip &strip) const;
@@ -659,6 +685,13 @@ class KeyframeStrip : public ::KeyframeActionStrip {
*/
bool channelbag_remove(ChannelBag &channelbag_to_remove);
/**
* Remove all strip data for the given slot.
*
* Typically only called from Strip::slot_data_remove().
*/
void slot_data_remove(slot_handle_t slot_handle);
/** Return the channelbag's index, or -1 if there is none for this slot handle. */
int64_t find_channelbag_index(const ChannelBag &channelbag) const;

View File

@@ -306,6 +306,17 @@ int64_t Action::find_layer_index(const Layer &layer) const
return -1;
}
int64_t Action::find_slot_index(const Slot &slot) const
{
for (const int64_t slot_index : this->slots().index_range()) {
const Slot *visit_slot = this->slot(slot_index);
if (visit_slot == &slot) {
return slot_index;
}
}
return -1;
}
blender::Span<const Slot *> Action::slots() const
{
return blender::Span<Slot *>{reinterpret_cast<Slot **>(this->slot_array), this->slot_array_num};
@@ -482,6 +493,48 @@ Slot &Action::slot_add_for_id(const ID &animated_id)
return slot;
}
static void slot_ptr_destructor(ActionSlot **dna_slot_ptr)
{
Slot &slot = (*dna_slot_ptr)->wrap();
MEM_delete(&slot);
};
bool Action::slot_remove(Slot &slot_to_remove)
{
/* Check that this slot belongs to this Action. */
const int64_t slot_index = this->find_slot_index(slot_to_remove);
if (slot_index < 0) {
return false;
}
/* Remove the slot's data from each layer. */
for (Layer *layer : this->layers()) {
layer->slot_data_remove(slot_to_remove.handle);
}
/* Un-assign this slot from its users. Only do this if the list of users is valid, */
for (ID *user : slot_to_remove.runtime_users()) {
/* Sanity check: make sure the slot is still assigned, before un-assigning anything. */
std::optional<std::pair<Action *, Slot *>> action_and_slot = get_action_slot_pair(*user);
BLI_assert_msg(action_and_slot, "Slot user has no Action assigned");
BLI_assert_msg(action_and_slot->first == this, "Slot user has other Action assigned");
BLI_assert_msg(action_and_slot->second == &slot_to_remove,
"Slot user has other Slot assigned");
if (!action_and_slot || action_and_slot->first != this ||
action_and_slot->second != &slot_to_remove)
{
continue;
}
this->assign_id(nullptr, *user);
}
/* Remove the actual slot. */
dna::array::remove_index(
&this->slot_array, &this->slot_array_num, nullptr, slot_index, slot_ptr_destructor);
return true;
}
void Action::slot_active_set(const slot_handle_t slot_handle)
{
for (Slot *slot : slots()) {
@@ -723,6 +776,13 @@ int64_t Layer::find_strip_index(const Strip &strip) const
return -1;
}
void Layer::slot_data_remove(const slot_handle_t slot_handle)
{
for (Strip *strip : this->strips()) {
strip->slot_data_remove(slot_handle);
}
}
/* ----- ActionSlot implementation ----------- */
Slot::Slot()
@@ -895,6 +955,14 @@ void Slot::name_ensure_prefix()
*reinterpret_cast<short *>(this->name) = this->idtype;
}
void Strip::slot_data_remove(const slot_handle_t slot_handle)
{
switch (this->type()) {
case Type::Keyframe:
this->as<KeyframeStrip>().slot_data_remove(slot_handle);
}
}
/* ----- Functions ----------- */
bool assign_action(Action &action, ID &animated_id)
@@ -1224,6 +1292,15 @@ bool KeyframeStrip::channelbag_remove(ChannelBag &channelbag_to_remove)
return true;
}
void KeyframeStrip::slot_data_remove(const slot_handle_t slot_handle)
{
ChannelBag *channelbag = this->channelbag_for_slot(slot_handle);
if (!channelbag) {
return;
}
this->channelbag_remove(*channelbag);
}
const FCurve *ChannelBag::fcurve_find(const FCurveDescriptor fcurve_descriptor) const
{
return animrig::fcurve_find(this->fcurves(), fcurve_descriptor);

View File

@@ -244,6 +244,103 @@ TEST_F(ActionLayersTest, add_slot_multiple)
EXPECT_EQ(2, slot_suzanne.handle);
}
TEST_F(ActionLayersTest, slot_remove)
{
{ /* Canary test: removing a just-created slot on an otherwise empty Action should work. */
Slot &slot = action->slot_add();
EXPECT_EQ(1, slot.handle);
EXPECT_EQ(1, action->last_slot_handle);
EXPECT_TRUE(action->slot_remove(slot));
EXPECT_EQ(1, action->last_slot_handle)
<< "Removing a slot should not change the last-used slot handle.";
EXPECT_EQ(0, action->slot_array_num);
}
{ /* Removing a non-existing slot should return false. */
Slot slot;
EXPECT_FALSE(action->slot_remove(slot));
}
{ /* Removing a slot should remove its ChannelBag. */
Slot &slot = action->slot_add();
const slot_handle_t slot_handle = slot.handle;
EXPECT_EQ(2, slot.handle);
EXPECT_EQ(2, action->last_slot_handle);
/* Create an F-Curve in a ChannelBag for the slot. */
action->layer_keystrip_ensure();
KeyframeStrip &strip = action->layer(0)->strip(0)->as<KeyframeStrip>();
ChannelBag &channelbag = strip.channelbag_for_slot_ensure(slot);
channelbag.fcurve_create_unique(bmain, {"location", 1});
/* Remove the slot. */
EXPECT_TRUE(action->slot_remove(slot));
EXPECT_EQ(2, action->last_slot_handle)
<< "Removing a slot should not change the last-used slot handle.";
EXPECT_EQ(0, action->slot_array_num);
/* Check that its channelbag is gone. */
ChannelBag *found_cbag = strip.channelbag_for_slot(slot_handle);
EXPECT_EQ(found_cbag, nullptr);
}
{ /* Removing one slot should leave the other two in place. */
Slot &slot1 = action->slot_add();
Slot &slot2 = action->slot_add();
Slot &slot3 = action->slot_add();
EXPECT_EQ(3, slot1.handle);
EXPECT_EQ(4, slot2.handle);
EXPECT_EQ(5, slot3.handle);
EXPECT_EQ(5, action->last_slot_handle);
/* For referencing the slot handle after the slot is removed. */
const slot_handle_t slot2_handle = slot2.handle;
/* Create a Channelbag for each slot. */
action->layer_keystrip_ensure();
KeyframeStrip &strip = action->layer(0)->strip(0)->as<KeyframeStrip>();
strip.channelbag_for_slot_ensure(slot1);
strip.channelbag_for_slot_ensure(slot2);
strip.channelbag_for_slot_ensure(slot3);
/* Remove the slot. */
EXPECT_TRUE(action->slot_remove(slot2));
EXPECT_EQ(5, action->last_slot_handle);
/* Check the correct slot + channelbag are removed. */
EXPECT_EQ(action->slot_for_handle(slot1.handle), &slot1);
EXPECT_EQ(action->slot_for_handle(slot2_handle), nullptr);
EXPECT_EQ(action->slot_for_handle(slot3.handle), &slot3);
EXPECT_NE(strip.channelbag_for_slot(slot1.handle), nullptr);
EXPECT_EQ(strip.channelbag_for_slot(slot2_handle), nullptr);
EXPECT_NE(strip.channelbag_for_slot(slot3.handle), nullptr);
}
{ /* Removing an in-use slot should un-assign it from its users. */
Slot &slot = action->slot_add_for_id(cube->id);
action->assign_id(&slot, cube->id);
ASSERT_TRUE(slot.runtime_users().contains(&cube->id));
ASSERT_EQ(cube->adt->slot_handle, slot.handle);
ASSERT_TRUE(action->slot_remove(slot));
EXPECT_EQ(cube->adt->slot_handle, Slot::unassigned);
}
{ /* Creating a slot after removing one should not reuse its handle. */
action->last_slot_handle = 3; /* To create independence between sub-tests. */
Slot &slot1 = action->slot_add();
ASSERT_EQ(4, slot1.handle);
ASSERT_EQ(4, action->last_slot_handle);
ASSERT_TRUE(action->slot_remove(slot1));
Slot &slot2 = action->slot_add();
EXPECT_EQ(5, slot2.handle);
EXPECT_EQ(5, action->last_slot_handle);
}
}
TEST_F(ActionLayersTest, action_assign_id)
{
/* Assign to the only, 'virgin' Slot, should always work. */

View File

@@ -202,6 +202,23 @@ static ActionSlot *rna_Action_slots_new(bAction *dna_action,
return slot;
}
void rna_Action_slots_remove(bAction *dna_action,
bContext *C,
ReportList *reports,
PointerRNA *slot_ptr)
{
animrig::Action &action = dna_action->wrap();
animrig::Slot &slot = rna_data_slot(slot_ptr);
if (!action.slot_remove(slot)) {
BKE_report(reports, RPT_ERROR, "This slot does not belong to this Action");
return;
}
RNA_POINTER_INVALIDATE(slot_ptr);
WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr);
DEG_id_tag_update(&action.id, ID_RECALC_ANIMATION);
}
static void rna_iterator_action_layers_begin(CollectionPropertyIterator *iter, PointerRNA *ptr)
{
animrig::Action &action = rna_action(ptr);
@@ -1689,7 +1706,7 @@ static void rna_def_action_slots(BlenderRNA *brna, PropertyRNA *cprop)
/* Animation.slots.new(...) */
func = RNA_def_function(srna, "new", "rna_Action_slots_new");
RNA_def_function_ui_description(func, "Add a slot to the animation");
RNA_def_function_ui_description(func, "Add a slot to the Action");
RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS);
parm = RNA_def_pointer(
func,
@@ -1704,6 +1721,15 @@ static void rna_def_action_slots(BlenderRNA *brna, PropertyRNA *cprop)
parm = RNA_def_pointer(func, "slot", "ActionSlot", "", "Newly created action slot");
RNA_def_function_return(func, parm);
/* Animation.slots.remove(layer) */
func = RNA_def_function(srna, "remove", "rna_Action_slots_remove");
RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS);
RNA_def_function_ui_description(func,
"Remove the slot from the Action, including all animation that "
"is associated with that slot");
parm = RNA_def_pointer(func, "action_slot", "ActionSlot", "Action Slot", "The slot to remove");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED | PARM_RNAPTR);
}
static void rna_def_action_layers(BlenderRNA *brna, PropertyRNA *cprop)