Files
test2/source/blender/animrig/intern/action_test.cc
Nathan Vegdahl c4e2e09c65 Cleanup: Anim: rename identifier_prefix_for_idtype to idtype_string
The previous name was confusingly similar to the new method
`identifier_prefix()` that was introduced in #133983. This new name
better distinguishes them while also reflecting its actual functionality
better and being shorter.

Pull Request: https://projects.blender.org/blender/blender/pulls/134046
2025-02-04 16:55:22 +01:00

2148 lines
86 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_fcurve.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_action_defaults.h"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include <limits>
#include "CLG_log.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
TEST(action, low_level_initialisation)
{
bAction *action = static_cast<bAction *>(BKE_id_new_nomain(ID_AC, "ACNewAction"));
EXPECT_NE(action->last_slot_handle, 0)
<< "bAction::last_slot_handle should not be initialised to 0";
BKE_id_free(nullptr, action);
}
class ActionLayersTest : public testing::Test {
public:
Main *bmain;
Action *action;
Object *cube;
Object *suzanne;
Object *bob;
static void SetUpTestSuite()
{
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
CLG_init();
/* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */
BKE_idtype_init();
}
static void TearDownTestSuite()
{
CLG_exit();
}
void SetUp() override
{
bmain = BKE_main_new();
action = static_cast<Action *>(BKE_id_new(bmain, ID_AC, "ACÄnimåtië"));
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
suzanne = BKE_object_add_only_object(bmain, OB_EMPTY, "OBSuzanne");
bob = BKE_object_add_only_object(bmain, OB_EMPTY, "OBBob");
}
void TearDown() override
{
BKE_main_free(bmain);
}
};
TEST_F(ActionLayersTest, add_layer)
{
Layer &layer = action->layer_add("layer name");
EXPECT_EQ(action->layer(0), &layer);
EXPECT_EQ("layer name", std::string(layer.name));
EXPECT_EQ(1.0f, layer.influence) << "Expected DNA defaults to be used.";
EXPECT_EQ(0, action->layer_active_index)
<< "Expected newly added layer to become the active layer.";
ASSERT_EQ(0, layer.strips().size()) << "Expected newly added layer to have no strip.";
}
TEST_F(ActionLayersTest, add_layer__reset_idroot)
{
/* An empty Action is a valid legacy Action, and thus can have its idroot set to a non-zero
* value. If such an Action gets a layer, it no longer is a valid legacy Action, and thus its
* idtype should be reset to zero. */
action->idroot = ID_CA; /* Fake that this was assigned to a camera data-block. */
ASSERT_NE(0, action->idroot) << "action->idroot should not be zero at the start of this test.";
action->layer_add("layer name");
EXPECT_EQ(0, action->idroot)
<< "action->idroot should get reset when the Action becomes layered.";
}
TEST_F(ActionLayersTest, remove_layer)
{
Layer &layer0 = action->layer_add("Test Læür nul");
Layer &layer1 = action->layer_add("Test Læür één");
Layer &layer2 = action->layer_add("Test Læür twee");
/* Add some strips to check that they are freed correctly too (implicitly by the
* memory leak checker). */
layer0.strip_add(*action, Strip::Type::Keyframe);
layer1.strip_add(*action, Strip::Type::Keyframe);
layer2.strip_add(*action, Strip::Type::Keyframe);
{ /* Test removing a layer that is not owned. */
Action *other_anim = static_cast<Action *>(BKE_id_new(bmain, ID_AC, "ACOtherAnim"));
Layer &other_layer = other_anim->layer_add("Another Layer");
EXPECT_FALSE(action->layer_remove(other_layer))
<< "Removing a layer not owned by the Action should be gracefully rejected";
BKE_id_free(bmain, &other_anim->id);
}
EXPECT_TRUE(action->layer_remove(layer1));
EXPECT_EQ(2, action->layers().size());
EXPECT_STREQ(layer0.name, action->layer(0)->name);
EXPECT_STREQ(layer2.name, action->layer(1)->name);
EXPECT_TRUE(action->layer_remove(layer2));
EXPECT_EQ(1, action->layers().size());
EXPECT_STREQ(layer0.name, action->layer(0)->name);
EXPECT_TRUE(action->layer_remove(layer0));
EXPECT_EQ(0, action->layers().size());
}
TEST_F(ActionLayersTest, add_strip)
{
Layer &layer = action->layer_add("Test Læür");
Strip &strip = layer.strip_add(*action, Strip::Type::Keyframe);
ASSERT_EQ(1, layer.strips().size());
EXPECT_EQ(&strip, layer.strip(0));
constexpr float inf = std::numeric_limits<float>::infinity();
EXPECT_EQ(-inf, strip.frame_start) << "Expected strip to be infinite.";
EXPECT_EQ(inf, strip.frame_end) << "Expected strip to be infinite.";
EXPECT_EQ(0, strip.frame_offset) << "Expected infinite strip to have no offset.";
Strip &another_strip = layer.strip_add(*action, Strip::Type::Keyframe);
ASSERT_EQ(2, layer.strips().size());
EXPECT_EQ(&another_strip, layer.strip(1));
EXPECT_EQ(-inf, another_strip.frame_start) << "Expected strip to be infinite.";
EXPECT_EQ(inf, another_strip.frame_end) << "Expected strip to be infinite.";
EXPECT_EQ(0, another_strip.frame_offset) << "Expected infinite strip to have no offset.";
/* Add some keys to check that also the strip data is freed correctly. */
const KeyframeSettings settings = get_keyframe_settings(false);
Slot &slot = action->slot_add();
strip.data<StripKeyframeData>(*action).keyframe_insert(
bmain, slot, {"location", 0}, {1.0f, 47.0f}, settings);
another_strip.data<StripKeyframeData>(*action).keyframe_insert(
bmain, slot, {"location", 0}, {1.0f, 47.0f}, settings);
}
TEST_F(ActionLayersTest, remove_strip)
{
Layer &layer = action->layer_add("Test Læür");
Strip &strip0 = layer.strip_add(*action, Strip::Type::Keyframe);
Strip &strip1 = layer.strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer.strip_add(*action, Strip::Type::Keyframe);
Strip &strip3 = layer.strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data0 = strip0.data<StripKeyframeData>(*action);
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
StripKeyframeData &strip_data3 = strip3.data<StripKeyframeData>(*action);
/* Add some keys to check that also the strip data is freed correctly. */
const KeyframeSettings settings = get_keyframe_settings(false);
Slot &slot = action->slot_add();
strip_data0.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data1.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 48.0f}, settings);
strip_data2.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 49.0f}, settings);
strip_data3.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 50.0f}, settings);
EXPECT_EQ(4, action->strip_keyframe_data().size());
EXPECT_EQ(0, strip0.data_index);
EXPECT_EQ(1, strip1.data_index);
EXPECT_EQ(2, strip2.data_index);
EXPECT_EQ(3, strip3.data_index);
EXPECT_TRUE(layer.strip_remove(*action, strip1));
EXPECT_EQ(3, action->strip_keyframe_data().size());
EXPECT_EQ(3, layer.strips().size());
EXPECT_EQ(&strip0, layer.strip(0));
EXPECT_EQ(&strip2, layer.strip(1));
EXPECT_EQ(&strip3, layer.strip(2));
EXPECT_EQ(0, strip0.data_index);
EXPECT_EQ(2, strip2.data_index);
EXPECT_EQ(1, strip3.data_index); /* Swapped in when removing strip 1's data. */
EXPECT_EQ(&strip_data0, &strip0.data<StripKeyframeData>(*action));
EXPECT_EQ(&strip_data2, &strip2.data<StripKeyframeData>(*action));
EXPECT_EQ(&strip_data3, &strip3.data<StripKeyframeData>(*action));
EXPECT_TRUE(layer.strip_remove(*action, strip2));
EXPECT_EQ(2, action->strip_keyframe_data().size());
EXPECT_EQ(2, layer.strips().size());
EXPECT_EQ(&strip0, layer.strip(0));
EXPECT_EQ(&strip3, layer.strip(1));
EXPECT_EQ(0, strip0.data_index);
EXPECT_EQ(1, strip3.data_index);
EXPECT_EQ(&strip_data0, &strip0.data<StripKeyframeData>(*action));
EXPECT_EQ(&strip_data3, &strip3.data<StripKeyframeData>(*action));
EXPECT_TRUE(layer.strip_remove(*action, strip3));
EXPECT_EQ(1, action->strip_keyframe_data().size());
EXPECT_EQ(1, layer.strips().size());
EXPECT_EQ(&strip0, layer.strip(0));
EXPECT_EQ(0, strip0.data_index);
EXPECT_EQ(&strip_data0, &strip0.data<StripKeyframeData>(*action));
EXPECT_TRUE(layer.strip_remove(*action, strip0));
EXPECT_EQ(0, action->strip_keyframe_data().size());
EXPECT_EQ(0, layer.strips().size());
{ /* Test removing a strip that is not owned. */
Layer &other_layer = action->layer_add("Another Layer");
Strip &other_strip = other_layer.strip_add(*action, Strip::Type::Keyframe);
EXPECT_FALSE(layer.strip_remove(*action, other_strip))
<< "Removing a strip not owned by the layer should be gracefully rejected";
}
}
/* NOTE: this test creates strip instances in a bespoke way for the purpose of
* exercising the strip removal code, because at the time of writing we don't
* have a proper API for creating strip instances. When such an API is added,
* this test should be updated to use it. */
TEST_F(ActionLayersTest, remove_strip_instances)
{
Layer &layer = action->layer_add("Test Læür");
Strip &strip0 = layer.strip_add(*action, Strip::Type::Keyframe);
Strip &strip1 = layer.strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer.strip_add(*action, Strip::Type::Keyframe);
/* Make on of the strips an instance of another. */
strip0.data_index = strip1.data_index;
StripKeyframeData &strip_data_0_1 = strip0.data<StripKeyframeData>(*action);
StripKeyframeData &strip_data_2 = strip2.data<StripKeyframeData>(*action);
/* Add some keys to check that also the strip data is freed correctly. */
const KeyframeSettings settings = get_keyframe_settings(false);
Slot &slot = action->slot_add();
strip_data_0_1.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data_2.keyframe_insert(bmain, slot, {"location", 0}, {1.0f, 48.0f}, settings);
EXPECT_EQ(3, action->strip_keyframe_data().size());
EXPECT_EQ(1, strip0.data_index);
EXPECT_EQ(1, strip1.data_index);
EXPECT_EQ(2, strip2.data_index);
/* Removing an instance should not delete the underlying data as long as there
* is still another strip using it. */
EXPECT_TRUE(layer.strip_remove(*action, strip1));
EXPECT_EQ(3, action->strip_keyframe_data().size());
EXPECT_EQ(2, layer.strips().size());
EXPECT_EQ(&strip0, layer.strip(0));
EXPECT_EQ(&strip2, layer.strip(1));
EXPECT_EQ(1, strip0.data_index);
EXPECT_EQ(2, strip2.data_index);
EXPECT_EQ(&strip_data_0_1, &strip0.data<StripKeyframeData>(*action));
EXPECT_EQ(&strip_data_2, &strip2.data<StripKeyframeData>(*action));
/* Removing the last user of strip data should also delete the data. */
EXPECT_TRUE(layer.strip_remove(*action, strip0));
EXPECT_EQ(2, action->strip_keyframe_data().size());
EXPECT_EQ(1, layer.strips().size());
EXPECT_EQ(&strip2, layer.strip(0));
EXPECT_EQ(1, strip2.data_index);
EXPECT_EQ(&strip_data_2, &strip2.data<StripKeyframeData>(*action));
}
TEST_F(ActionLayersTest, add_slot)
{
{ /* Creating an 'unused' Slot should just be called 'Slot'. */
Slot &slot = action->slot_add();
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 1, action->last_slot_handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 1, slot.handle);
EXPECT_STREQ("XXSlot", slot.identifier);
EXPECT_EQ(0, slot.idtype);
}
{ /* Creating a Slot for a specific ID should name it after the ID. */
Slot &slot = action->slot_add_for_id(cube->id);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 2, action->last_slot_handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 2, slot.handle);
EXPECT_STREQ(cube->id.name, slot.identifier);
EXPECT_EQ(ID_OB, slot.idtype);
}
{ /* Creating a Slot for a specific ID that already had a slot assigned before should name it
* after that previous slot. This should also ensure that the first two characters are actually
* correct for the ID type. */
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
STRNCPY_UTF8(adt->last_slot_identifier, "$$Kübuš 😹");
Slot &slot = action->slot_add_for_id(cube->id);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 3, action->last_slot_handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 3, slot.handle);
EXPECT_STREQ("Kübuš 😹", slot.identifier + 2)
<< "The last-assigned slot name should be reused";
EXPECT_STREQ("OBKübuš 😹", slot.identifier)
<< "The ID type encoded in the slot identifier should be correct";
EXPECT_EQ(ID_OB, slot.idtype);
}
}
TEST_F(ActionLayersTest, add_slot__reset_idroot)
{
/* An empty Action is a valid legacy Action, and thus can have its idroot set
* to a non-zero value. If such an Action gets a slot, it no longer is a
* valid legacy Action, and thus its idtype should be reset to zero. */
action->idroot = ID_CA; /* Fake that this was assigned to a camera data-block. */
ASSERT_NE(0, action->idroot) << "action->idroot should not be zero at the start of this test.";
action->slot_add();
EXPECT_EQ(0, action->idroot)
<< "action->idroot should get reset when the Action becomes layered.";
}
TEST_F(ActionLayersTest, add_slot_multiple)
{
Slot &slot_cube = action->slot_add();
Slot &slot_suzanne = action->slot_add();
EXPECT_TRUE(assign_action(action, cube->id));
EXPECT_EQ(assign_action_slot(&slot_cube, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_TRUE(assign_action(action, suzanne->id));
EXPECT_EQ(assign_action_slot(&slot_suzanne, suzanne->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 2, action->last_slot_handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 1, slot_cube.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 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(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 1, slot.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 1, action->last_slot_handle);
EXPECT_TRUE(action->slot_remove(slot));
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 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(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 2, slot.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 2, action->last_slot_handle);
/* Create an F-Curve in a Channelbag for the slot. */
action->layer_keystrip_ensure();
StripKeyframeData &strip_data = action->layer(0)->strip(0)->data<StripKeyframeData>(*action);
Channelbag &channelbag = strip_data.channelbag_for_slot_ensure(slot);
channelbag.fcurve_create_unique(bmain, {"location", 1});
/* Remove the slot. */
EXPECT_TRUE(action->slot_remove(slot));
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 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_data.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(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 3, slot1.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 4, slot2.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 5, slot3.handle);
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 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 Channel-bag for each slot. */
action->layer_keystrip_ensure();
StripKeyframeData &strip_data = action->layer(0)->strip(0)->data<StripKeyframeData>(*action);
strip_data.channelbag_for_slot_ensure(slot1);
strip_data.channelbag_for_slot_ensure(slot2);
strip_data.channelbag_for_slot_ensure(slot3);
/* Remove the slot. */
EXPECT_TRUE(action->slot_remove(slot2));
EXPECT_EQ(DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE + 5, action->last_slot_handle);
/* Check the correct slot + channel-bag 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_data.channelbag_for_slot(slot1.handle), nullptr);
EXPECT_EQ(strip_data.channelbag_for_slot(slot2_handle), nullptr);
EXPECT_NE(strip_data.channelbag_for_slot(slot3.handle), nullptr);
}
{ /* Removing an in-use slot doesn't un-assign it from its users.
* This is not that important, but it covers the current behavior. */
Slot &slot = action->slot_add_for_id(cube->id);
ASSERT_EQ(assign_action_and_slot(action, &slot, cube->id), ActionSlotAssignmentResult::OK);
ASSERT_TRUE(slot.runtime_users().contains(&cube->id));
ASSERT_EQ(cube->adt->slot_handle, slot.handle);
const slot_handle_t removed_slot_handle = slot.handle;
ASSERT_TRUE(action->slot_remove(slot));
EXPECT_EQ(cube->adt->slot_handle, removed_slot_handle);
}
{ /* 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, slot_move_to_index)
{
Slot &slot_a = action->slot_add_for_id_type(ID_ME);
Slot &slot_b = action->slot_add_for_id_type(ID_CA);
Slot &slot_cube = action->slot_add_for_id(cube->id);
Slot &slot_suzanne = action->slot_add_for_id(suzanne->id);
assign_action_and_slot(action, &slot_cube, cube->id);
assign_action_and_slot(action, &slot_suzanne, suzanne->id);
const slot_handle_t handle_a = slot_a.handle;
const slot_handle_t handle_b = slot_b.handle;
const slot_handle_t handle_cube = slot_cube.handle;
const slot_handle_t handle_suzanne = slot_suzanne.handle;
ASSERT_EQ(action->slot(0)->handle, handle_a);
ASSERT_EQ(action->slot(0)->idtype_string(), "ME");
ASSERT_EQ(action->slot(1)->handle, handle_b);
ASSERT_EQ(action->slot(1)->idtype_string(), "CA");
ASSERT_EQ(action->slot(2)->handle, handle_cube);
ASSERT_EQ(action->slot(2)->idtype_string(), "OB");
ASSERT_EQ(action->slot(2)->users(*bmain)[0], &cube->id);
ASSERT_EQ(action->slot(3)->handle, handle_suzanne);
ASSERT_EQ(action->slot(3)->idtype_string(), "OB");
ASSERT_EQ(action->slot(3)->users(*bmain)[0], &suzanne->id);
/* First "move" a slot to its own location, which should do nothing. */
action->slot_move_to_index(slot_b, 1);
EXPECT_EQ(action->slot(0)->handle, handle_a);
EXPECT_EQ(action->slot(0)->idtype_string(), "ME");
EXPECT_EQ(action->slot(1)->handle, handle_b);
EXPECT_EQ(action->slot(1)->idtype_string(), "CA");
EXPECT_EQ(action->slot(2)->handle, handle_cube);
EXPECT_EQ(action->slot(2)->idtype_string(), "OB");
EXPECT_EQ(action->slot(2)->users(*bmain)[0], &cube->id);
EXPECT_EQ(action->slot(3)->handle, handle_suzanne);
EXPECT_EQ(action->slot(3)->idtype_string(), "OB");
EXPECT_EQ(action->slot(3)->users(*bmain)[0], &suzanne->id);
/* Then move slots around in various ways. */
action->slot_move_to_index(slot_a, 2);
EXPECT_EQ(action->slot(0)->handle, handle_b);
EXPECT_EQ(action->slot(0)->idtype_string(), "CA");
EXPECT_EQ(action->slot(1)->handle, handle_cube);
EXPECT_EQ(action->slot(1)->idtype_string(), "OB");
EXPECT_EQ(action->slot(1)->users(*bmain)[0], &cube->id);
EXPECT_EQ(action->slot(2)->handle, handle_a);
EXPECT_EQ(action->slot(2)->idtype_string(), "ME");
EXPECT_EQ(action->slot(3)->handle, handle_suzanne);
EXPECT_EQ(action->slot(3)->idtype_string(), "OB");
EXPECT_EQ(action->slot(3)->users(*bmain)[0], &suzanne->id);
action->slot_move_to_index(slot_suzanne, 1);
EXPECT_EQ(action->slot(0)->handle, handle_b);
EXPECT_EQ(action->slot(0)->idtype_string(), "CA");
EXPECT_EQ(action->slot(1)->handle, handle_suzanne);
EXPECT_EQ(action->slot(1)->idtype_string(), "OB");
EXPECT_EQ(action->slot(1)->users(*bmain)[0], &suzanne->id);
EXPECT_EQ(action->slot(2)->handle, handle_cube);
EXPECT_EQ(action->slot(2)->idtype_string(), "OB");
EXPECT_EQ(action->slot(2)->users(*bmain)[0], &cube->id);
EXPECT_EQ(action->slot(3)->handle, handle_a);
EXPECT_EQ(action->slot(3)->idtype_string(), "ME");
action->slot_move_to_index(slot_cube, 3);
EXPECT_EQ(action->slot(0)->handle, handle_b);
EXPECT_EQ(action->slot(0)->idtype_string(), "CA");
EXPECT_EQ(action->slot(1)->handle, handle_suzanne);
EXPECT_EQ(action->slot(1)->idtype_string(), "OB");
EXPECT_EQ(action->slot(1)->users(*bmain)[0], &suzanne->id);
EXPECT_EQ(action->slot(2)->handle, handle_a);
EXPECT_EQ(action->slot(2)->idtype_string(), "ME");
EXPECT_EQ(action->slot(3)->handle, handle_cube);
EXPECT_EQ(action->slot(3)->idtype_string(), "OB");
EXPECT_EQ(action->slot(3)->users(*bmain)[0], &cube->id);
action->slot_move_to_index(slot_suzanne, 0);
EXPECT_EQ(action->slot(0)->handle, handle_suzanne);
EXPECT_EQ(action->slot(0)->idtype_string(), "OB");
EXPECT_EQ(action->slot(0)->users(*bmain)[0], &suzanne->id);
EXPECT_EQ(action->slot(1)->handle, handle_b);
EXPECT_EQ(action->slot(1)->idtype_string(), "CA");
EXPECT_EQ(action->slot(2)->handle, handle_a);
EXPECT_EQ(action->slot(2)->idtype_string(), "ME");
EXPECT_EQ(action->slot(3)->handle, handle_cube);
EXPECT_EQ(action->slot(3)->idtype_string(), "OB");
EXPECT_EQ(action->slot(3)->users(*bmain)[0], &cube->id);
}
TEST_F(ActionLayersTest, action_assign_id)
{
/* Assign to the only, 'virgin' Slot, should always work. */
Slot &slot_cube = action->slot_add();
ASSERT_NE(nullptr, slot_cube.runtime);
ASSERT_STREQ(slot_cube.identifier, "XXSlot");
ASSERT_EQ(assign_action_and_slot(action, &slot_cube, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(slot_cube.handle, cube->adt->slot_handle);
EXPECT_STREQ(slot_cube.identifier, "OBSlot");
EXPECT_STREQ(slot_cube.identifier, cube->adt->last_slot_identifier)
<< "The slot identifier should be copied to the adt";
EXPECT_TRUE(slot_cube.users(*bmain).contains(&cube->id))
<< "Expecting Cube to be registered as animated by its slot.";
/* Assign another ID to the same Slot. */
ASSERT_EQ(assign_action_and_slot(action, &slot_cube, suzanne->id),
ActionSlotAssignmentResult::OK);
EXPECT_STREQ(slot_cube.identifier, "OBSlot");
EXPECT_STREQ(slot_cube.identifier, cube->adt->last_slot_identifier)
<< "The slot identifier should be copied to the adt";
EXPECT_TRUE(slot_cube.users(*bmain).contains(&cube->id))
<< "Expecting Suzanne to be registered as animated by the Cube slot.";
{ /* Assign Cube to another action+slot without unassigning first. */
Action *another_anim = static_cast<Action *>(BKE_id_new(bmain, ID_AC, "ACOtherAnim"));
Slot &another_slot = another_anim->slot_add();
ASSERT_EQ(assign_action_and_slot(another_anim, &another_slot, cube->id),
ActionSlotAssignmentResult::OK);
EXPECT_FALSE(slot_cube.users(*bmain).contains(&cube->id))
<< "Expecting Cube to no longer be registered as user of its old slot.";
EXPECT_TRUE(another_slot.users(*bmain).contains(&cube->id))
<< "Expecting Cube to be registered as user of its new slot.";
}
{ /* Assign Cube to another slot of the same Action, this should work. */
ASSERT_EQ(assign_action_and_slot(action, &slot_cube, cube->id),
ActionSlotAssignmentResult::OK);
const int user_count_pre = action->id.us;
Slot &slot_cube_2 = action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, &slot_cube_2, cube->id),
ActionSlotAssignmentResult::OK);
ASSERT_EQ(action->id.us, user_count_pre)
<< "Assigning to a different slot of the same Action should _not_ change the user "
"count of that Action";
EXPECT_FALSE(slot_cube.users(*bmain).contains(&cube->id))
<< "Expecting Cube to no longer be registered as animated by the Cube slot.";
EXPECT_TRUE(slot_cube_2.users(*bmain).contains(&cube->id))
<< "Expecting Cube to be registered as animated by the 'cube_2' slot.";
}
{ /* Unassign the Action. */
const int user_count_pre = action->id.us;
EXPECT_TRUE(unassign_action(cube->id));
ASSERT_EQ(action->id.us, user_count_pre - 1)
<< "Unassigning an Action should lower its user count";
ASSERT_EQ(2, action->slots().size()) << "Expecting the Action to have two Slots";
EXPECT_FALSE(action->slot(0)->users(*bmain).contains(&cube->id))
<< "Expecting Cube to no longer be registered as animated by any slot.";
EXPECT_FALSE(action->slot(1)->users(*bmain).contains(&cube->id))
<< "Expecting Cube to no longer be registered as animated by any slot.";
}
/* Assign Cube to another 'virgin' slot. This should not cause a name
* collision between the Slots. */
Slot &another_slot_cube = action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, &another_slot_cube, cube->id),
ActionSlotAssignmentResult::OK);
EXPECT_EQ(another_slot_cube.handle, cube->adt->slot_handle);
EXPECT_STREQ("OBSlot.002", another_slot_cube.identifier) << "The slot should be uniquely named";
EXPECT_STREQ("OBSlot.002", cube->adt->last_slot_identifier)
<< "The slot identifier should be copied to the adt";
EXPECT_TRUE(another_slot_cube.users(*bmain).contains(&cube->id))
<< "Expecting Cube to be registered as animated by the 'another_slot_cube' slot.";
/* Create an ID of another type. This should not be assignable to this slot. */
ID *mesh = static_cast<ID *>(BKE_id_new_nomain(ID_ME, "Mesh"));
ASSERT_TRUE(assign_action(action, *mesh));
EXPECT_EQ(assign_action_slot(&slot_cube, *mesh), ActionSlotAssignmentResult::SlotNotSuitable)
<< "Mesh should not be animatable by an Object slot";
EXPECT_FALSE(another_slot_cube.users(*bmain).contains(mesh))
<< "Expecting Mesh to not be registered as animated by the 'slot_cube' slot.";
BKE_id_free(nullptr, mesh);
}
TEST_F(ActionLayersTest, rename_slot)
{
Slot &slot_cube = action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, &slot_cube, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(slot_cube.handle, cube->adt->slot_handle);
EXPECT_STREQ("OBSlot", slot_cube.identifier);
EXPECT_STREQ(slot_cube.identifier, cube->adt->last_slot_identifier)
<< "The slot identifier should be copied to the adt";
action->slot_identifier_define(slot_cube, "OBNew Slot Name");
EXPECT_STREQ("OBNew Slot Name", slot_cube.identifier);
/* At this point the slot identifier will not have been copied to the cube
* AnimData. However, I don't want to test for that here, as it's not exactly
* desirable behavior, but more of a side-effect of the current
* implementation. */
action->slot_identifier_propagate(*bmain, slot_cube);
EXPECT_STREQ("OBNew Slot Name", cube->adt->last_slot_identifier);
/* Rename via the display name, which should propagate to the ADT. */
action->slot_display_name_set(*bmain, slot_cube, "Slot's New Display Name");
EXPECT_STREQ("OBSlot's New Display Name", slot_cube.identifier);
EXPECT_STREQ("OBSlot's New Display Name", cube->adt->last_slot_identifier);
/* Finally, do another rename, do NOT call the propagate function, then
* unassign. This should still result in the correct slot name being stored
* on the ADT. */
action->slot_identifier_define(slot_cube, "OBEven Newer Name");
EXPECT_TRUE(unassign_action(cube->id));
EXPECT_STREQ("OBEven Newer Name", cube->adt->last_slot_identifier);
}
TEST_F(ActionLayersTest, slot_identifier_ensure_prefix)
{
class AccessibleSlot : public Slot {
public:
void identifier_ensure_prefix()
{
Slot::identifier_ensure_prefix();
}
};
Slot &raw_slot = action->slot_add();
AccessibleSlot &slot = static_cast<AccessibleSlot &>(raw_slot);
ASSERT_STREQ("XXSlot", slot.identifier);
ASSERT_EQ(0, slot.idtype);
/* Check defaults, idtype zeroed. */
slot.identifier_ensure_prefix();
EXPECT_STREQ("XXSlot", slot.identifier);
/* idtype CA, default name. */
slot.idtype = ID_CA;
slot.identifier_ensure_prefix();
EXPECT_STREQ("CASlot", slot.identifier);
/* idtype ME, explicit name of other idtype. */
action->slot_identifier_define(slot, "CANewName");
slot.idtype = ID_ME;
slot.identifier_ensure_prefix();
EXPECT_STREQ("MENewName", slot.identifier);
/* Zeroing out idtype. */
slot.idtype = 0;
slot.identifier_ensure_prefix();
EXPECT_STREQ("XXNewName", slot.identifier);
}
TEST_F(ActionLayersTest, slot_identifier_prefix)
{
Slot &slot = action->slot_add();
EXPECT_EQ("XX", slot.idtype_string());
EXPECT_EQ("XX", slot.identifier_prefix());
slot.idtype = ID_CA;
EXPECT_EQ("CA", slot.idtype_string());
EXPECT_EQ("XX", slot.identifier_prefix());
slot.identifier_ensure_prefix();
EXPECT_EQ("CA", slot.idtype_string());
EXPECT_EQ("CA", slot.identifier_prefix());
}
TEST_F(ActionLayersTest, rename_slot_identifier_collision)
{
Slot &slot1 = action->slot_add();
Slot &slot2 = action->slot_add();
action->slot_identifier_define(slot1, "New Slot Name");
action->slot_identifier_define(slot2, "New Slot Name");
EXPECT_STREQ("New Slot Name", slot1.identifier);
EXPECT_STREQ("New Slot Name.001", slot2.identifier);
}
TEST_F(ActionLayersTest, generic_slot_for_autoassign)
{
/* ===
* Empty case, no slots exist yet and the ID doesn't even have an AnimData. */
EXPECT_EQ(nullptr, generic_slot_for_autoassign(cube->id, *this->action, ""));
/* ===
* Slot exists with the same name & type as the ID, but the ID doesn't have any AnimData yet.
* These should nevertheless be matched up. */
Slot &slot = action->slot_add();
slot.handle = 327;
STRNCPY_UTF8(slot.identifier, "OBKüüübus");
slot.idtype = GS(cube->id.name);
EXPECT_EQ(&slot, generic_slot_for_autoassign(cube->id, *this->action, ""));
/* ===
* Slot exists with the same name & type as the ID, and the ID has an AnimData with the same
* slot identifier, but a different slot_handle. Since the Action has not yet been
* assigned to this ID, the slot_handle should be ignored, and the slot identifier used for
* matching. */
/* Create a slot with a handle that should be ignored. */
Slot &other_slot = action->slot_add();
other_slot.handle = 47;
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
adt->action = nullptr;
/* Configure adt to use the handle of one slot, and the identifier of the other. */
adt->slot_handle = other_slot.handle;
STRNCPY_UTF8(adt->last_slot_identifier, slot.identifier);
EXPECT_EQ(&slot,
generic_slot_for_autoassign(cube->id, *this->action, cube->adt->last_slot_identifier));
/* ===
* Assigned slot info exists, but doesn't match anything in the action data of the cube. This
* should fall back to using the ID name. */
adt->slot_handle = 161;
STRNCPY_UTF8(adt->last_slot_identifier, "¿¿What's this??");
EXPECT_EQ(&slot,
generic_slot_for_autoassign(cube->id, *this->action, cube->adt->last_slot_identifier));
}
TEST_F(ActionLayersTest, generic_slot_for_autoassign_untyped_wildcarding)
{
/* Test the untyped slot "wildcard" behavior, where OBSlot should be chosen when the last slot
* identifier was "XXSlot", and vice versa. */
/* ===
* Action has OBSlot, last-used slot is XXSlot. Should pick OBSlot. */
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
STRNCPY_UTF8(adt->last_slot_identifier, "XXSlot");
Slot &ob_slot = action->slot_add_for_id_type(ID_OB);
action->slot_identifier_define(ob_slot, "OBSlot");
EXPECT_EQ(&ob_slot,
generic_slot_for_autoassign(cube->id, *this->action, adt->last_slot_identifier));
/* ===
* Action has OBSlot and XXSlot, last-used slot is XXSlot. Should pick OBSlot. */
Slot &xx_slot = action->slot_add();
action->slot_identifier_define(xx_slot, "XXSlot");
ASSERT_FALSE(xx_slot.has_idtype());
ASSERT_STREQ("XXSlot", xx_slot.identifier);
ASSERT_STREQ("XXSlot", adt->last_slot_identifier);
EXPECT_EQ(&ob_slot,
generic_slot_for_autoassign(cube->id, *this->action, adt->last_slot_identifier));
/* ===
* Action has OBSlot and XXSlot, last-used slot is OBSlot. Should pick OBSlot. */
STRNCPY_UTF8(adt->last_slot_identifier, "OBSlot");
EXPECT_EQ(&ob_slot,
generic_slot_for_autoassign(cube->id, *this->action, adt->last_slot_identifier));
/* ===
* Action has XXSlot, last-used slot is OBSlot. Should pick XXSlot. */
action->slot_remove(ob_slot);
ASSERT_STREQ("OBSlot", adt->last_slot_identifier);
EXPECT_EQ(&xx_slot,
generic_slot_for_autoassign(cube->id, *this->action, adt->last_slot_identifier));
}
TEST_F(ActionLayersTest, active_slot)
{
{ /* Empty case, no slots exist yet. */
EXPECT_EQ(nullptr, action->slot_active_get());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get());
}
{ /* Single slot case. */
Slot &slot_cube = *assign_action_ensure_slot_for_keying(*action, cube->id);
EXPECT_EQ(nullptr, action->slot_active_get())
<< "Adding the first slot should not change what is the active slot.";
action->slot_active_set(slot_cube.handle);
EXPECT_EQ(&slot_cube, action->slot_active_get())
<< "It should be possible to activate the only available slot";
EXPECT_TRUE(slot_cube.is_active());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get())
<< "It should be possible to de-activate the only available slot";
EXPECT_FALSE(slot_cube.is_active());
}
{
/* Multiple slots case. */
Slot &slot_cube = *action->slot(0);
action->slot_active_set(slot_cube.handle);
Slot &slot_suz = *assign_action_ensure_slot_for_keying(*action, suzanne->id);
Slot &slot_bob = *assign_action_ensure_slot_for_keying(*action, bob->id);
EXPECT_EQ(&slot_cube, action->slot_active_get())
<< "Adding a subsequent slot should not change what is the active slot.";
EXPECT_TRUE(slot_cube.is_active());
action->slot_active_set(slot_suz.handle);
EXPECT_EQ(&slot_suz, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_TRUE(slot_suz.is_active());
EXPECT_FALSE(slot_bob.is_active());
action->slot_active_set(slot_bob.handle);
EXPECT_EQ(&slot_bob, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_FALSE(slot_suz.is_active());
EXPECT_TRUE(slot_bob.is_active());
action->slot_active_set(Slot::unassigned);
EXPECT_EQ(nullptr, action->slot_active_get());
EXPECT_FALSE(slot_cube.is_active());
EXPECT_FALSE(slot_suz.is_active());
EXPECT_FALSE(slot_bob.is_active());
}
}
TEST_F(ActionLayersTest, assign_action_ensure_slot_for_keying)
{
{ /* Slotless Action, should create a typed slot. */
Action &action = action_add(*this->bmain, "ACEmpty");
Slot *chosen_slot = assign_action_ensure_slot_for_keying(action, cube->id);
ASSERT_NE(nullptr, chosen_slot);
EXPECT_EQ(ID_OB, chosen_slot->idtype);
EXPECT_STREQ("OBKüüübus", chosen_slot->identifier);
}
{ /* Single slot with same name as ID, Action not yet assigned. Should assign the Action and the
slot. */
Action &action = action_add(*this->bmain, "ACAction");
const Slot &slot_for_id = action.slot_add_for_id(cube->id);
Slot *chosen_slot = assign_action_ensure_slot_for_keying(action, cube->id);
ASSERT_NE(nullptr, chosen_slot);
EXPECT_EQ(&slot_for_id, chosen_slot) << "The expected slot should be chosen";
EXPECT_EQ(cube->adt->action, &action) << "The Action should be assigned";
EXPECT_EQ(cube->adt->slot_handle, chosen_slot->handle) << "The chosen slot should be assigned";
}
{ /* Single slot with same name as ID, Action already assigned but not the slot. Should create
* new slot. */
Action &action = action_add(*this->bmain, "ACAction");
const Slot &slot_for_id = action.slot_add_for_id(cube->id);
ASSERT_EQ(ActionSlotAssignmentResult::OK, assign_action_and_slot(&action, nullptr, cube->id));
Slot *chosen_slot = assign_action_ensure_slot_for_keying(action, cube->id);
ASSERT_NE(nullptr, chosen_slot);
EXPECT_NE(&slot_for_id, chosen_slot) << "A new slot should be chosen";
EXPECT_STREQ("OBKüüübus.001", chosen_slot->identifier);
EXPECT_EQ(cube->adt->action, &action) << "The Action should be assigned";
EXPECT_EQ(cube->adt->slot_handle, chosen_slot->handle) << "The chosen slot should be assigned";
}
{ /* Single untyped slot, Action already assigned but not the slot. Should assign the untyped
* slot. */
Action &action = action_add(*this->bmain, "ACAction");
/* Assign the Action before adding the untyped slot, otherwise the slot gets assigned & thus
* typed. */
ASSERT_EQ(ActionSlotAssignmentResult::OK, assign_action_and_slot(&action, nullptr, cube->id));
Slot &untyped_slot = action.slot_add();
action.slot_identifier_define(untyped_slot, "XXJust A Slot");
Slot *chosen_slot = assign_action_ensure_slot_for_keying(action, cube->id);
ASSERT_NE(nullptr, chosen_slot);
EXPECT_EQ(&untyped_slot, chosen_slot) << "The untyped slot should be chosen";
EXPECT_TRUE(untyped_slot.has_idtype()) << "Slot should have gotten an ID type";
EXPECT_STREQ("OBJust A Slot", untyped_slot.identifier);
EXPECT_EQ(cube->adt->action, &action) << "The Action should be assigned";
EXPECT_EQ(cube->adt->slot_handle, chosen_slot->handle) << "The chosen slot should be assigned";
}
}
TEST_F(ActionLayersTest, strip)
{
constexpr float inf = std::numeric_limits<float>::infinity();
Layer &layer0 = action->layer_add("Test Læür nul");
Strip &strip = layer0.strip_add(*action, Strip::Type::Keyframe);
strip.resize(-inf, inf);
EXPECT_TRUE(strip.contains_frame(0.0f));
EXPECT_TRUE(strip.contains_frame(-100000.0f));
EXPECT_TRUE(strip.contains_frame(100000.0f));
EXPECT_TRUE(strip.is_last_frame(inf));
strip.resize(1.0f, 2.0f);
EXPECT_FALSE(strip.contains_frame(0.0f))
<< "Strip should not contain frames before its first frame";
EXPECT_TRUE(strip.contains_frame(1.0f)) << "Strip should contain its first frame.";
EXPECT_TRUE(strip.contains_frame(2.0f)) << "Strip should contain its last frame.";
EXPECT_FALSE(strip.contains_frame(2.0001f))
<< "Strip should not contain frames after its last frame";
EXPECT_FALSE(strip.is_last_frame(1.0f));
EXPECT_FALSE(strip.is_last_frame(1.5f));
EXPECT_FALSE(strip.is_last_frame(1.9999f));
EXPECT_TRUE(strip.is_last_frame(2.0f));
EXPECT_FALSE(strip.is_last_frame(2.0001f));
/* Same test as above, but with much larger end frame number. This is 2 hours at 24 FPS. */
strip.resize(1.0f, 172800.0f);
EXPECT_TRUE(strip.contains_frame(172800.0f)) << "Strip should contain its last frame.";
EXPECT_FALSE(strip.contains_frame(172800.1f))
<< "Strip should not contain frames after its last frame";
/* You can't get much closer to the end frame before it's considered equal. */
EXPECT_FALSE(strip.is_last_frame(172799.925f));
EXPECT_TRUE(strip.is_last_frame(172800.0f));
EXPECT_FALSE(strip.is_last_frame(172800.075f));
}
TEST_F(ActionLayersTest, KeyframeStrip__keyframe_insert)
{
Slot &slot = action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, &slot, cube->id), ActionSlotAssignmentResult::OK);
Layer &layer = action->layer_add("Kübus layer");
Strip &strip = layer.strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
const KeyframeSettings settings = get_keyframe_settings(false);
SingleKeyingResult result_loc_a = strip_data.keyframe_insert(
bmain, slot, {"location", 0}, {1.0f, 47.0f}, settings);
ASSERT_EQ(SingleKeyingResult::SUCCESS, result_loc_a)
<< "Expected keyframe insertion to be successful";
/* Check the strip was created correctly, with the channels for the slot. */
ASSERT_EQ(1, strip_data.channelbags().size());
Channelbag *channels = strip_data.channelbag(0);
EXPECT_EQ(slot.handle, channels->slot_handle);
/* Insert a second key, should insert into the same FCurve as before. */
SingleKeyingResult result_loc_b = strip_data.keyframe_insert(
bmain, slot, {"location", 0}, {5.0f, 47.1f}, settings);
EXPECT_EQ(SingleKeyingResult::SUCCESS, result_loc_b);
ASSERT_EQ(1, channels->fcurves().size()) << "Expect insertion with the same (slot/rna "
"path/array index) tuple to go into the same FCurve";
EXPECT_EQ(2, channels->fcurves()[0]->totvert)
<< "Expect insertion with the same (slot/rna path/array index) tuple to go into the same "
"FCurve";
EXPECT_EQ(47.0f, evaluate_fcurve(channels->fcurves()[0], 1.0f));
EXPECT_EQ(47.1f, evaluate_fcurve(channels->fcurves()[0], 5.0f));
/* Insert another key for another property, should create another FCurve. */
SingleKeyingResult result_rot = strip_data.keyframe_insert(
bmain, slot, {"rotation_quaternion", 0}, {1.0f, 0.25f}, settings);
EXPECT_EQ(SingleKeyingResult::SUCCESS, result_rot);
ASSERT_EQ(2, channels->fcurves().size()) << "Expected a second FCurve to be created.";
EXPECT_EQ(2, channels->fcurves()[0]->totvert);
EXPECT_EQ(1, channels->fcurves()[1]->totvert);
}
TEST_F(ActionLayersTest, is_action_assignable_to)
{
EXPECT_TRUE(is_action_assignable_to(nullptr, ID_OB))
<< "nullptr Actions should be assignable to any type.";
EXPECT_TRUE(is_action_assignable_to(nullptr, ID_CA))
<< "nullptr Actions should be assignable to any type.";
EXPECT_TRUE(is_action_assignable_to(action, ID_OB))
<< "Empty Actions should be assignable to any type.";
EXPECT_TRUE(is_action_assignable_to(action, ID_CA))
<< "Empty Actions should be assignable to any type.";
/* Make the Action a legacy one. */
FCurve fake_fcurve;
BLI_addtail(&action->curves, &fake_fcurve);
ASSERT_FALSE(action->is_empty());
ASSERT_TRUE(action->is_action_legacy());
ASSERT_EQ(0, action->idroot);
EXPECT_TRUE(is_action_assignable_to(action, ID_OB))
<< "Legacy Actions with idroot=0 should be assignable to any type.";
EXPECT_TRUE(is_action_assignable_to(action, ID_CA))
<< "Legacy Actions with idroot=0 should be assignable to any type.";
/* Set the legacy idroot. */
action->idroot = ID_CA;
EXPECT_FALSE(is_action_assignable_to(action, ID_OB))
<< "Legacy Actions with idroot=ID_CA should NOT be assignable to ID_OB.";
EXPECT_TRUE(is_action_assignable_to(action, ID_CA))
<< "Legacy Actions with idroot=CA should be assignable to ID_CA.";
/* Make the Action a layered one. */
BLI_poptail(&action->curves);
action->layer_add("layer");
ASSERT_EQ(0, action->idroot) << "Adding a layer should clear the idroot.";
EXPECT_TRUE(is_action_assignable_to(action, ID_OB))
<< "Layered Actions should be assignable to any type.";
EXPECT_TRUE(is_action_assignable_to(action, ID_CA))
<< "Layered Actions should be assignable to any type.";
}
TEST_F(ActionLayersTest, action_slot_get_id_for_keying__empty_action)
{
EXPECT_TRUE(assign_action(action, cube->id));
/* Double-check that the action is considered empty for the test. */
EXPECT_TRUE(action->is_empty());
/* None should return an ID, since there are no slots yet which could have this ID assigned.
* Assignment of the Action itself (cube) shouldn't matter. */
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, 0, &cube->id));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, 0, nullptr));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, 0, &suzanne->id));
}
TEST_F(ActionLayersTest, action_slot_get_id_for_keying__legacy_action)
{
FCurve *fcurve = action_fcurve_ensure_legacy(bmain, action, nullptr, nullptr, {"location", 0});
EXPECT_FALSE(fcurve == nullptr);
EXPECT_TRUE(assign_action(action, cube->id));
/* Double-check that the action is considered legacy for the test. */
EXPECT_TRUE(action->is_action_legacy());
/* A `primary_id` that uses the action should get returned. Every other case
* should return nullptr. */
EXPECT_EQ(&cube->id, action_slot_get_id_for_keying(*bmain, *action, 0, &cube->id));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, 0, nullptr));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, 0, &suzanne->id));
}
TEST_F(ActionLayersTest, action_slot_get_id_for_keying__layered_action)
{
Slot &slot = action->slot_add();
/* Double-check that the action is considered layered for the test. */
EXPECT_TRUE(action->is_action_layered());
/* A slot with no users should never return a user. */
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, slot.handle, nullptr));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, slot.handle, &cube->id));
/* A slot with precisely one user should always return that user. */
ASSERT_EQ(assign_action_and_slot(action, &slot, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(&cube->id, action_slot_get_id_for_keying(*bmain, *action, slot.handle, nullptr));
EXPECT_EQ(&cube->id, action_slot_get_id_for_keying(*bmain, *action, slot.handle, &cube->id));
EXPECT_EQ(&cube->id, action_slot_get_id_for_keying(*bmain, *action, slot.handle, &suzanne->id));
/* A slot with more than one user should return the passed `primary_id` if it
* is among its users, and nullptr otherwise. */
ASSERT_EQ(assign_action_and_slot(action, &slot, suzanne->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(&cube->id, action_slot_get_id_for_keying(*bmain, *action, slot.handle, &cube->id));
EXPECT_EQ(&suzanne->id,
action_slot_get_id_for_keying(*bmain, *action, slot.handle, &suzanne->id));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, slot.handle, nullptr));
EXPECT_EQ(nullptr, action_slot_get_id_for_keying(*bmain, *action, slot.handle, &bob->id));
}
TEST_F(ActionLayersTest, conversion_to_layered)
{
EXPECT_TRUE(action->is_empty());
FCurve *legacy_fcu_0 = action_fcurve_ensure_legacy(
bmain, action, "Test", nullptr, {"location", 0});
FCurve *legacy_fcu_1 = action_fcurve_ensure_legacy(
bmain, action, "Test", nullptr, {"location", 1});
KeyframeSettings settings;
settings.handle = HD_AUTO;
settings.interpolation = BEZT_IPO_BEZ;
settings.keyframe_type = BEZT_KEYTYPE_KEYFRAME;
insert_vert_fcurve(legacy_fcu_0, {0, 0}, settings, INSERTKEY_NOFLAGS);
insert_vert_fcurve(legacy_fcu_0, {1, 1}, settings, INSERTKEY_NOFLAGS);
add_fmodifier(&legacy_fcu_1->modifiers, FMODIFIER_TYPE_NOISE, legacy_fcu_1);
Action *converted = convert_to_layered_action(*bmain, *action);
ASSERT_TRUE(converted != action);
EXPECT_STREQ(converted->id.name, "ACACÄnimåtië_layered");
Strip *strip = converted->layer(0)->strip(0);
StripKeyframeData &strip_data = strip->data<StripKeyframeData>(*converted);
Channelbag *bag = strip_data.channelbag(0);
ASSERT_EQ(bag->fcurve_array_num, 2);
ASSERT_EQ(bag->fcurve_array[0]->totvert, 2);
ASSERT_EQ(BLI_listbase_count(&action->groups), 1);
ASSERT_EQ(BLI_listbase_count(&converted->groups), 0);
ASSERT_EQ(bag->channel_groups().size(), 1);
bActionGroup *group = bag->channel_group(0);
ASSERT_EQ(group->fcurve_range_length, 2);
ASSERT_STREQ(group->name, "Test");
ASSERT_TRUE(bag->fcurve_array[0]->modifiers.first == nullptr);
ASSERT_TRUE(bag->fcurve_array[1]->modifiers.first != nullptr);
Action *long_name_action = static_cast<Action *>(BKE_id_new(
bmain, ID_AC, "name_for_an_action_that_is_exactly_64_chars_which_is_MAX_ID_NAME"));
action_fcurve_ensure_legacy(bmain, long_name_action, "Long", nullptr, {"location", 0});
converted = convert_to_layered_action(*bmain, *long_name_action);
/* AC gets added automatically by Blender, the long name is shortened to make space for
* "_layered". */
EXPECT_STREQ(converted->id.name,
"ACname_for_an_action_that_is_exactly_64_chars_which_is_MA_layered");
}
TEST_F(ActionLayersTest, conversion_to_layered_action_groups)
{
EXPECT_TRUE(action->is_empty());
action_fcurve_ensure_legacy(bmain, action, "Test", nullptr, {"location", 0});
action_fcurve_ensure_legacy(bmain, action, "Test", nullptr, {"rotation_euler", 1});
action_fcurve_ensure_legacy(bmain, action, "Test_Two", nullptr, {"scale", 1});
action_fcurve_ensure_legacy(bmain, action, "Test_Three", nullptr, {"show_name", 1});
action_fcurve_ensure_legacy(bmain, action, "Test_Rename", nullptr, {"show_axis", 1});
bActionGroup *rename_group = static_cast<bActionGroup *>(BLI_findlink(&action->groups, 3));
ASSERT_NE(rename_group, nullptr);
ASSERT_STREQ(rename_group->name, "Test_Rename");
/* Forcing a duplicate name which was allowed by legacy actions. */
strcpy(rename_group->name, "Test");
Action *converted = convert_to_layered_action(*bmain, *action);
Strip *strip = converted->layer(0)->strip(0);
StripKeyframeData &strip_data = strip->data<StripKeyframeData>(*converted);
Channelbag *bag = strip_data.channelbag(0);
ASSERT_EQ(BLI_listbase_count(&converted->groups), 0);
ASSERT_EQ(bag->channel_groups().size(), 4);
bActionGroup *test_group = bag->channel_group(0);
EXPECT_STREQ(test_group->name, "Test");
EXPECT_EQ(test_group->fcurve_range_length, 2);
bActionGroup *test_two_group = bag->channel_group(1);
EXPECT_STREQ(test_two_group->name, "Test_Two");
EXPECT_EQ(test_two_group->fcurve_range_length, 1);
EXPECT_STREQ(bag->fcurve_array[test_two_group->fcurve_range_start]->rna_path, "scale");
bActionGroup *test_three_group = bag->channel_group(2);
EXPECT_STREQ(test_three_group->name, "Test_Three");
EXPECT_EQ(test_three_group->fcurve_range_length, 1);
EXPECT_STREQ(bag->fcurve_array[test_three_group->fcurve_range_start]->rna_path, "show_name");
bActionGroup *test_rename_group = bag->channel_group(3);
EXPECT_STREQ(test_rename_group->name, "Test.001");
EXPECT_EQ(test_rename_group->fcurve_range_length, 1);
EXPECT_STREQ(bag->fcurve_array[test_rename_group->fcurve_range_start]->rna_path, "show_axis");
ASSERT_NE(converted, action);
}
TEST_F(ActionLayersTest, empty_to_layered)
{
ASSERT_TRUE(action->is_empty());
Action *converted = convert_to_layered_action(*bmain, *action);
ASSERT_TRUE(converted != action);
ASSERT_TRUE(converted->is_action_layered());
ASSERT_FALSE(converted->is_action_legacy());
}
TEST_F(ActionLayersTest, action_move_slot)
{
Action *action_2 = static_cast<Action *>(BKE_id_new(bmain, ID_AC, "Action 2"));
EXPECT_TRUE(action->is_empty());
Slot &slot_cube = action->slot_add();
Slot &slot_suzanne = action_2->slot_add();
EXPECT_EQ(assign_action_and_slot(action, &slot_cube, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(assign_action_and_slot(action_2, &slot_suzanne, suzanne->id),
ActionSlotAssignmentResult::OK);
PointerRNA cube_rna_pointer = RNA_id_pointer_create(&cube->id);
PointerRNA suzanne_rna_pointer = RNA_id_pointer_create(&suzanne->id);
action_fcurve_ensure(bmain, action, "Test", &cube_rna_pointer, {"location", 0});
action_fcurve_ensure(bmain, action, "Test", &cube_rna_pointer, {"rotation_euler", 1});
action_fcurve_ensure(bmain, action_2, "Test_2", &suzanne_rna_pointer, {"location", 0});
action_fcurve_ensure(bmain, action_2, "Test_2", &suzanne_rna_pointer, {"rotation_euler", 1});
ASSERT_EQ(action->layer_array_num, 1);
ASSERT_EQ(action_2->layer_array_num, 1);
Layer *layer_1 = action->layer(0);
Layer *layer_2 = action_2->layer(0);
ASSERT_EQ(layer_1->strip_array_num, 1);
ASSERT_EQ(layer_2->strip_array_num, 1);
StripKeyframeData &strip_data_1 = layer_1->strip(0)->data<StripKeyframeData>(*action);
StripKeyframeData &strip_data_2 = layer_2->strip(0)->data<StripKeyframeData>(*action_2);
ASSERT_EQ(strip_data_1.channelbag_array_num, 1);
ASSERT_EQ(strip_data_2.channelbag_array_num, 1);
Channelbag *bag_1 = strip_data_1.channelbag(0);
Channelbag *bag_2 = strip_data_2.channelbag(0);
ASSERT_EQ(bag_1->fcurve_array_num, 2);
ASSERT_EQ(bag_2->fcurve_array_num, 2);
move_slot(*bmain, slot_suzanne, *action_2, *action);
ASSERT_EQ(strip_data_1.channelbag_array_num, 2);
ASSERT_EQ(strip_data_2.channelbag_array_num, 0);
ASSERT_EQ(action->slot_array_num, 2);
ASSERT_EQ(action_2->slot_array_num, 0);
/* Action should have been reassigned. */
ASSERT_EQ(action, cube->adt->action);
ASSERT_EQ(action, suzanne->adt->action);
}
/*-----------------------------------------------------------*/
/* Allocate fcu->bezt, and also return a unique_ptr to it for easily freeing the memory. */
static void allocate_keyframes(FCurve &fcu, const size_t num_keyframes)
{
fcu.bezt = MEM_cnew_array<BezTriple>(num_keyframes, __func__);
}
/* Append keyframe, assumes that fcu->bezt is allocated and has enough space. */
static void add_keyframe(FCurve &fcu, float x, float y)
{
/* The insert_keyframe functions are in the editors, so we cannot link to those here. */
BezTriple the_keyframe;
memset(&the_keyframe, 0, sizeof(the_keyframe));
/* Copied from insert_vert_fcurve() in `keyframing.cc`. */
the_keyframe.vec[0][0] = x - 1.0f;
the_keyframe.vec[0][1] = y;
the_keyframe.vec[1][0] = x;
the_keyframe.vec[1][1] = y;
the_keyframe.vec[2][0] = x + 1.0f;
the_keyframe.vec[2][1] = y;
memcpy(&fcu.bezt[fcu.totvert], &the_keyframe, sizeof(the_keyframe));
fcu.totvert++;
}
static void add_fcurve_to_action(Action &action, FCurve &fcu)
{
Slot &slot = action.slot_array_num > 0 ? *action.slot(0) : action.slot_add();
action.layer_keystrip_ensure();
StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
Channelbag &cbag = strip_data.channelbag_for_slot_ensure(slot);
cbag.fcurve_append(fcu);
}
class ActionQueryTest : public testing::Test {
public:
Main *bmain;
static void SetUpTestSuite()
{
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
CLG_init();
/* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */
BKE_idtype_init();
}
static void TearDownTestSuite()
{
CLG_exit();
}
void SetUp() override
{
bmain = BKE_main_new();
}
void TearDown() override
{
BKE_main_free(bmain);
}
Action &action_new()
{
return *static_cast<Action *>(BKE_id_new(bmain, ID_AC, "ACÄnimåtië"));
}
};
TEST_F(ActionQueryTest, BKE_action_frame_range_calc)
{
/* No FCurves. */
{
const Action &empty = action_new();
EXPECT_EQ((float2{0.0f, 0.0f}), empty.get_frame_range_of_keys(false));
}
/* One curve with one key. */
{
FCurve &fcu = *MEM_cnew<FCurve>(__func__);
allocate_keyframes(fcu, 1);
add_keyframe(fcu, 1.0f, 2.0f);
Action &action = action_new();
add_fcurve_to_action(action, fcu);
const float2 frame_range = action.get_frame_range_of_keys(false);
EXPECT_FLOAT_EQ(frame_range[0], 1.0f);
EXPECT_FLOAT_EQ(frame_range[1], 1.0f);
}
/* Two curves with one key each on different frames. */
{
FCurve &fcu1 = *MEM_cnew<FCurve>(__func__);
FCurve &fcu2 = *MEM_cnew<FCurve>(__func__);
allocate_keyframes(fcu1, 1);
allocate_keyframes(fcu2, 1);
add_keyframe(fcu1, 1.0f, 2.0f);
add_keyframe(fcu2, 1.5f, 2.0f);
Action &action = action_new();
add_fcurve_to_action(action, fcu1);
add_fcurve_to_action(action, fcu2);
const float2 frame_range = action.get_frame_range_of_keys(false);
EXPECT_FLOAT_EQ(frame_range[0], 1.0f);
EXPECT_FLOAT_EQ(frame_range[1], 1.5f);
}
/* One curve with two keys. */
{
FCurve &fcu = *MEM_cnew<FCurve>(__func__);
allocate_keyframes(fcu, 2);
add_keyframe(fcu, 1.0f, 2.0f);
add_keyframe(fcu, 1.5f, 2.0f);
Action &action = action_new();
add_fcurve_to_action(action, fcu);
const float2 frame_range = action.get_frame_range_of_keys(false);
EXPECT_FLOAT_EQ(frame_range[0], 1.0f);
EXPECT_FLOAT_EQ(frame_range[1], 1.5f);
}
/* TODO: action with fcurve modifiers. */
}
/*-----------------------------------------------------------*/
class ChannelbagTest : public testing::Test {
public:
Channelbag *channelbag;
static void SetUpTestSuite() {}
static void TearDownTestSuite() {}
void SetUp() override
{
channelbag = new Channelbag();
}
void TearDown() override
{
delete channelbag;
}
};
TEST_F(ChannelbagTest, fcurve_move_to_index)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "group0"});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "group0"});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "group1"});
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "group1"});
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, std::nullopt});
ASSERT_EQ(5, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
bActionGroup &group0 = *channelbag->channel_group(0);
bActionGroup &group1 = *channelbag->channel_group(1);
/* Moving an fcurve to where it already is should be fine. */
channelbag->fcurve_move_to_index(fcu0, 0);
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu1, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group1, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
/* Move to first. */
channelbag->fcurve_move_to_index(fcu4, 0);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&fcu4, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(2));
EXPECT_EQ(&fcu2, channelbag->fcurve(3));
EXPECT_EQ(&fcu3, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu4.grp);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group1, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu3.grp);
/* Move to last. */
channelbag->fcurve_move_to_index(fcu1, 4);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&fcu4, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu1, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu4.grp);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group1, fcu3.grp);
EXPECT_EQ(nullptr, fcu1.grp);
/* Move to middle. */
channelbag->fcurve_move_to_index(fcu4, 2);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu4, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu1, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu2.grp);
EXPECT_EQ(&group1, fcu4.grp);
EXPECT_EQ(&group1, fcu3.grp);
EXPECT_EQ(nullptr, fcu1.grp);
}
TEST_F(ChannelbagTest, channel_group_create)
{
ASSERT_TRUE(channelbag->channel_groups().is_empty());
bActionGroup &group0 = channelbag->channel_group_create("Foo");
ASSERT_EQ(channelbag->channel_groups().size(), 1);
EXPECT_EQ(StringRef{group0.name}, StringRef{"Foo"});
EXPECT_EQ(group0.fcurve_range_start, 0);
EXPECT_EQ(group0.fcurve_range_length, 0);
EXPECT_EQ(&group0, channelbag->channel_group(0));
/* Set for testing purposes. Does not reflect actual fcurves in this test. */
group0.fcurve_range_length = 2;
bActionGroup &group1 = channelbag->channel_group_create("Bar");
ASSERT_EQ(channelbag->channel_groups().size(), 2);
EXPECT_EQ(StringRef{group1.name}, StringRef{"Bar"});
EXPECT_EQ(group1.fcurve_range_start, 2);
EXPECT_EQ(group1.fcurve_range_length, 0);
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
/* Set for testing purposes. Does not reflect actual fcurves in this test. */
group1.fcurve_range_length = 1;
bActionGroup &group2 = channelbag->channel_group_create("Yar");
ASSERT_EQ(channelbag->channel_groups().size(), 3);
EXPECT_EQ(StringRef{group2.name}, StringRef{"Yar"});
EXPECT_EQ(group2.fcurve_range_start, 3);
EXPECT_EQ(group2.fcurve_range_length, 0);
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(&group2, channelbag->channel_group(2));
}
TEST_F(ChannelbagTest, channel_group_remove)
{
bActionGroup &group0 = channelbag->channel_group_create("Group0");
bActionGroup &group1 = channelbag->channel_group_create("Group1");
bActionGroup &group2 = channelbag->channel_group_create("Group2");
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "Group0"});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "Group0"});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "Group2"});
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "Group2"});
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, std::nullopt});
ASSERT_EQ(3, channelbag->channel_groups().size());
ASSERT_EQ(5, channelbag->fcurves().size());
/* Attempt to remove a group that's not in the channel bag. Shouldn't do
* anything. */
bActionGroup bogus;
EXPECT_EQ(false, channelbag->channel_group_remove(bogus));
ASSERT_EQ(3, channelbag->channel_groups().size());
ASSERT_EQ(5, channelbag->fcurves().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(&group2, channelbag->channel_group(2));
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu1, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group2, fcu2.grp);
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
/* Removing an empty group shouldn't affect the fcurves at all. */
EXPECT_EQ(true, channelbag->channel_group_remove(group1));
ASSERT_EQ(2, channelbag->channel_groups().size());
ASSERT_EQ(5, channelbag->fcurves().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group2, channelbag->channel_group(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu1, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group2, fcu2.grp);
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
/* Removing a group that's not at the end of the group array should move its
* fcurves to be just after the grouped fcurves. */
EXPECT_EQ(true, channelbag->channel_group_remove(group0));
ASSERT_EQ(1, channelbag->channel_groups().size());
ASSERT_EQ(5, channelbag->fcurves().size());
EXPECT_EQ(&group2, channelbag->channel_group(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(0));
EXPECT_EQ(&fcu3, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(&fcu1, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(nullptr, fcu1.grp);
EXPECT_EQ(&group2, fcu2.grp);
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
/* Removing a group at the end of the group array shouldn't move its
* fcurves. */
EXPECT_EQ(true, channelbag->channel_group_remove(group2));
ASSERT_EQ(0, channelbag->channel_groups().size());
ASSERT_EQ(5, channelbag->fcurves().size());
EXPECT_EQ(&fcu2, channelbag->fcurve(0));
EXPECT_EQ(&fcu3, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(&fcu1, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(nullptr, fcu1.grp);
EXPECT_EQ(nullptr, fcu2.grp);
EXPECT_EQ(nullptr, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
}
TEST_F(ChannelbagTest, channel_group_find)
{
bActionGroup &group0a = channelbag->channel_group_create("Foo");
bActionGroup &group1a = channelbag->channel_group_create("Bar");
bActionGroup &group2a = channelbag->channel_group_create("Yar");
bActionGroup *group0b = channelbag->channel_group_find("Foo");
bActionGroup *group1b = channelbag->channel_group_find("Bar");
bActionGroup *group2b = channelbag->channel_group_find("Yar");
EXPECT_EQ(&group0a, group0b);
EXPECT_EQ(&group1a, group1b);
EXPECT_EQ(&group2a, group2b);
EXPECT_EQ(nullptr, channelbag->channel_group_find("Wat"));
}
TEST_F(ChannelbagTest, channel_group_ensure)
{
bActionGroup &group0 = channelbag->channel_group_create("Foo");
bActionGroup &group1 = channelbag->channel_group_create("Bar");
EXPECT_EQ(channelbag->channel_groups().size(), 2);
EXPECT_EQ(&group0, &channelbag->channel_group_ensure("Foo"));
EXPECT_EQ(channelbag->channel_groups().size(), 2);
EXPECT_EQ(&group1, &channelbag->channel_group_ensure("Bar"));
EXPECT_EQ(channelbag->channel_groups().size(), 2);
bActionGroup &group2 = channelbag->channel_group_ensure("Yar");
ASSERT_EQ(channelbag->channel_groups().size(), 3);
EXPECT_EQ(&group2, channelbag->channel_group(2));
}
TEST_F(ChannelbagTest, channel_group_fcurve_creation)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, std::nullopt});
EXPECT_EQ(1, channelbag->fcurves().size());
EXPECT_TRUE(channelbag->channel_groups().is_empty());
/* If an fcurve already exists, then ensuring it with a channel group in the
* fcurve descriptor should NOT add it that group, nor should the group be
* created if it doesn't already exist. */
channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "group0"});
EXPECT_EQ(1, channelbag->fcurves().size());
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_TRUE(channelbag->channel_groups().is_empty());
/* Creating a new fcurve with a channel group in the fcurve descriptor should
* create the group and put the fcurve in it. This also implies that the
* fcurve will be added before any non-grouped fcurves in the array. */
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "group0"});
ASSERT_EQ(2, channelbag->fcurves().size());
ASSERT_EQ(1, channelbag->channel_groups().size());
bActionGroup &group0 = *channelbag->channel_group(0);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
/* Creating a new fcurve with a second channel group in the fcurve descriptor
* should create the group and put the fcurve in it. This also implies that
* the fcurve will be added before non-grouped fcurves, but after other
* grouped ones. */
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "group1"});
ASSERT_EQ(3, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
bActionGroup &group1 = *channelbag->channel_group(1);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
/* Creating a new fcurve with the first channel group again should put it at
* the end of that group. */
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "group0"});
ASSERT_EQ(4, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu3, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu0, channelbag->fcurve(3));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group0, fcu3.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
/* Finally, creating a new fcurve with the second channel group again should
* also put it at the end of that group. */
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, "group1"});
ASSERT_EQ(5, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu3, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu4, channelbag->fcurve(3));
EXPECT_EQ(&fcu0, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group0, fcu3.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group1, fcu4.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
}
TEST_F(ChannelbagTest, channel_group_fcurve_removal)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "group0"});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "group0"});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "group1"});
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "group1"});
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, std::nullopt});
ASSERT_EQ(5, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
bActionGroup &group0 = *channelbag->channel_group(0);
bActionGroup &group1 = *channelbag->channel_group(1);
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group1, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->fcurve_remove(fcu3);
ASSERT_EQ(4, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->fcurve_remove(fcu0);
ASSERT_EQ(3, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->fcurve_remove(fcu1);
ASSERT_EQ(2, channelbag->fcurves().size());
ASSERT_EQ(1, channelbag->channel_groups().size());
EXPECT_EQ(&group1, channelbag->channel_group(0));
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->fcurve_remove(fcu4);
ASSERT_EQ(1, channelbag->fcurves().size());
ASSERT_EQ(1, channelbag->channel_groups().size());
EXPECT_EQ(&group1, channelbag->channel_group(0));
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&group1, fcu2.grp);
channelbag->fcurve_remove(fcu2);
ASSERT_EQ(0, channelbag->fcurves().size());
ASSERT_EQ(0, channelbag->channel_groups().size());
}
TEST_F(ChannelbagTest, channel_group_move_to_index)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "group0"});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "group1"});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "group1"});
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "group2"});
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, std::nullopt});
ASSERT_EQ(5, channelbag->fcurves().size());
ASSERT_EQ(3, channelbag->channel_groups().size());
bActionGroup &group0 = *channelbag->channel_group(0);
bActionGroup &group1 = *channelbag->channel_group(1);
bActionGroup &group2 = *channelbag->channel_group(2);
channelbag->channel_group_move_to_index(group0, 2);
EXPECT_EQ(&group1, channelbag->channel_group(0));
EXPECT_EQ(&group2, channelbag->channel_group(1));
EXPECT_EQ(&group0, channelbag->channel_group(2));
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(2, group2.fcurve_range_start);
EXPECT_EQ(1, group2.fcurve_range_length);
EXPECT_EQ(3, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu3, channelbag->fcurve(2));
EXPECT_EQ(&fcu0, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group1, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->channel_group_move_to_index(group1, 1);
EXPECT_EQ(&group2, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(&group0, channelbag->channel_group(2));
EXPECT_EQ(0, group2.fcurve_range_start);
EXPECT_EQ(1, group2.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(3, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(&fcu3, channelbag->fcurve(0));
EXPECT_EQ(&fcu1, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(&fcu0, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(&group1, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(nullptr, fcu4.grp);
channelbag->channel_group_move_to_index(group0, 0);
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group2, channelbag->channel_group(1));
EXPECT_EQ(&group1, channelbag->channel_group(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group2.fcurve_range_start);
EXPECT_EQ(1, group2.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu3, channelbag->fcurve(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(2));
EXPECT_EQ(&fcu2, channelbag->fcurve(3));
EXPECT_EQ(&fcu4, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu0.grp);
EXPECT_EQ(&group2, fcu3.grp);
EXPECT_EQ(&group1, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
}
TEST_F(ChannelbagTest, channel_group_move_fcurve_into)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, std::nullopt});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, std::nullopt});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, std::nullopt});
bActionGroup &group0 = channelbag->channel_group_create("group0");
bActionGroup &group1 = channelbag->channel_group_create("group1");
ASSERT_EQ(3, channelbag->fcurves().size());
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu1, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
ASSERT_EQ(2, channelbag->channel_groups().size());
EXPECT_EQ(&group0, channelbag->channel_group(0));
EXPECT_EQ(&group1, channelbag->channel_group(1));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(0, group0.fcurve_range_length);
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(0, group1.fcurve_range_length);
channelbag->fcurve_assign_to_channel_group(fcu2, group1);
EXPECT_EQ(&fcu2, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(0, group0.fcurve_range_length);
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
channelbag->fcurve_assign_to_channel_group(fcu1, group0);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
channelbag->fcurve_assign_to_channel_group(fcu0, group1);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
channelbag->fcurve_assign_to_channel_group(fcu0, group0);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&fcu2, channelbag->fcurve(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(2, group0.fcurve_range_length);
EXPECT_EQ(2, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
channelbag->fcurve_assign_to_channel_group(fcu1, group1);
EXPECT_EQ(&fcu0, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu1, channelbag->fcurve(2));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
}
TEST_F(ChannelbagTest, channel_group_fcurve_ungroup)
{
FCurve &fcu0 = channelbag->fcurve_ensure(nullptr, {"fcu0", 0, std::nullopt, "group0"});
FCurve &fcu1 = channelbag->fcurve_ensure(nullptr, {"fcu1", 0, std::nullopt, "group0"});
FCurve &fcu2 = channelbag->fcurve_ensure(nullptr, {"fcu2", 0, std::nullopt, "group1"});
FCurve &fcu3 = channelbag->fcurve_ensure(nullptr, {"fcu3", 0, std::nullopt, "group1"});
FCurve &fcu4 = channelbag->fcurve_ensure(nullptr, {"fcu4", 0, std::nullopt, std::nullopt});
ASSERT_EQ(5, channelbag->fcurves().size());
ASSERT_EQ(2, channelbag->channel_groups().size());
bActionGroup &group0 = *channelbag->channel_group(0);
bActionGroup &group1 = *channelbag->channel_group(1);
/* Attempting to ungroup an fcurve that's not in the channel bag should fail. */
FCurve bogus = {};
EXPECT_FALSE(channelbag->fcurve_ungroup(bogus));
/* Attempting to ungroup an fcurve that's already ungrouped is fine. */
EXPECT_TRUE(channelbag->fcurve_ungroup(fcu4));
/* Ungroup each fcurve until all are ungrouped. */
EXPECT_TRUE(channelbag->fcurve_ungroup(fcu0));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(2, group1.fcurve_range_length);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu3, channelbag->fcurve(2));
EXPECT_EQ(&fcu4, channelbag->fcurve(3));
EXPECT_EQ(&fcu0, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(&group1, fcu3.grp);
EXPECT_EQ(nullptr, fcu4.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_TRUE(channelbag->fcurve_ungroup(fcu3));
EXPECT_EQ(0, group0.fcurve_range_start);
EXPECT_EQ(1, group0.fcurve_range_length);
EXPECT_EQ(1, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&fcu1, channelbag->fcurve(0));
EXPECT_EQ(&fcu2, channelbag->fcurve(1));
EXPECT_EQ(&fcu4, channelbag->fcurve(2));
EXPECT_EQ(&fcu0, channelbag->fcurve(3));
EXPECT_EQ(&fcu3, channelbag->fcurve(4));
EXPECT_EQ(&group0, fcu1.grp);
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(nullptr, fcu3.grp);
EXPECT_TRUE(channelbag->fcurve_ungroup(fcu1));
EXPECT_EQ(1, channelbag->channel_groups().size());
EXPECT_EQ(&group1, channelbag->channel_group(0));
EXPECT_EQ(0, group1.fcurve_range_start);
EXPECT_EQ(1, group1.fcurve_range_length);
EXPECT_EQ(&fcu2, channelbag->fcurve(0));
EXPECT_EQ(&fcu4, channelbag->fcurve(1));
EXPECT_EQ(&fcu0, channelbag->fcurve(2));
EXPECT_EQ(&fcu3, channelbag->fcurve(3));
EXPECT_EQ(&fcu1, channelbag->fcurve(4));
EXPECT_EQ(&group1, fcu2.grp);
EXPECT_EQ(nullptr, fcu4.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(nullptr, fcu3.grp);
EXPECT_EQ(nullptr, fcu1.grp);
EXPECT_TRUE(channelbag->fcurve_ungroup(fcu2));
EXPECT_EQ(0, channelbag->channel_groups().size());
EXPECT_EQ(&fcu4, channelbag->fcurve(0));
EXPECT_EQ(&fcu0, channelbag->fcurve(1));
EXPECT_EQ(&fcu3, channelbag->fcurve(2));
EXPECT_EQ(&fcu1, channelbag->fcurve(3));
EXPECT_EQ(&fcu2, channelbag->fcurve(4));
EXPECT_EQ(nullptr, fcu4.grp);
EXPECT_EQ(nullptr, fcu0.grp);
EXPECT_EQ(nullptr, fcu3.grp);
EXPECT_EQ(nullptr, fcu1.grp);
EXPECT_EQ(nullptr, fcu2.grp);
}
/*-----------------------------------------------------------*/
class ActionFCurveMoveTest : public testing::Test {
public:
Main *bmain;
static void SetUpTestSuite()
{
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
CLG_init();
/* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */
BKE_idtype_init();
}
static void TearDownTestSuite()
{
CLG_exit();
}
void SetUp() override
{
bmain = BKE_main_new();
}
void TearDown() override
{
BKE_main_free(bmain);
}
static FCurve *fcurve_create(const StringRefNull rna_path, const int array_index)
{
FCurve *fcurve = BKE_fcurve_create();
fcurve->rna_path = BLI_strdupn(rna_path.c_str(), array_index);
return fcurve;
};
};
TEST_F(ActionFCurveMoveTest, test_fcurve_move_legacy)
{
Action &action_src = action_add(*this->bmain, "SourceAction");
Action &action_dst = action_add(*this->bmain, "DestinationAction");
/* Add F-Curves to source Action. */
BLI_addtail(&action_src.curves, fcurve_create("source_prop", 0));
FCurve *fcurve_to_move = fcurve_create("source_prop", 2);
BLI_addtail(&action_src.curves, fcurve_to_move);
/* Add F-Curves to destination Action. */
BLI_addtail(&action_dst.curves, fcurve_create("dest_prop", 0));
ASSERT_TRUE(action_src.is_action_legacy());
ASSERT_TRUE(action_dst.is_action_legacy());
action_fcurve_move(action_dst, Slot::unassigned, action_src, *fcurve_to_move);
EXPECT_TRUE(action_src.is_action_legacy());
EXPECT_TRUE(action_dst.is_action_legacy());
EXPECT_EQ(-1, BLI_findindex(&action_src.curves, fcurve_to_move))
<< "F-Curve should no longer exist in source Action";
EXPECT_EQ(1, BLI_findindex(&action_dst.curves, fcurve_to_move))
<< "F-Curve should exist in destination Action";
EXPECT_EQ(1, BLI_listbase_count(&action_src.curves))
<< "Source Action should still have the other F-Curve";
EXPECT_EQ(2, BLI_listbase_count(&action_dst.curves))
<< "Destination Action should have its original and the moved F-Curve";
}
TEST_F(ActionFCurveMoveTest, test_fcurve_move_layered)
{
Action &action_src = action_add(*this->bmain, "SourceAction");
Action &action_dst = action_add(*this->bmain, "DestinationAction");
/* Add F-Curves to source Action. */
Slot &slot_src = action_src.slot_add();
action_src.layer_keystrip_ensure();
StripKeyframeData &strip_data_src = action_src.layer(0)->strip(0)->data<StripKeyframeData>(
action_src);
Channelbag &cbag_src = strip_data_src.channelbag_for_slot_ensure(slot_src);
cbag_src.fcurve_ensure(this->bmain, {"source_prop", 0});
FCurve &fcurve_to_move = cbag_src.fcurve_ensure(this->bmain, {"source_prop", 2});
bActionGroup &group_src = cbag_src.channel_group_create("Gröpje");
cbag_src.fcurve_assign_to_channel_group(fcurve_to_move, group_src);
/* Add F-Curves to destination Action. */
Slot &slot_dst = action_dst.slot_add();
action_dst.layer_keystrip_ensure();
StripKeyframeData &strip_data_dst = action_dst.layer(0)->strip(0)->data<StripKeyframeData>(
action_dst);
Channelbag &cbag_dst = strip_data_dst.channelbag_for_slot_ensure(slot_dst);
cbag_dst.fcurve_ensure(this->bmain, {"dest_prop", 0});
ASSERT_TRUE(action_src.is_action_layered());
ASSERT_TRUE(action_dst.is_action_layered());
action_fcurve_move(action_dst, slot_dst.handle, action_src, fcurve_to_move);
EXPECT_TRUE(action_src.is_action_layered());
EXPECT_TRUE(action_dst.is_action_layered());
EXPECT_EQ(nullptr, cbag_src.fcurve_find({fcurve_to_move.rna_path, fcurve_to_move.array_index}))
<< "F-Curve should no longer exist in source Action";
EXPECT_EQ(&fcurve_to_move,
cbag_dst.fcurve_find({fcurve_to_move.rna_path, fcurve_to_move.array_index}))
<< "F-Curve should exist in destination Action";
EXPECT_EQ(1, cbag_src.fcurves().size()) << "Source Action should still have the other F-Curve";
EXPECT_EQ(2, cbag_dst.fcurves().size())
<< "Destination Action should have its original and the moved F-Curve";
bActionGroup *group_dst = cbag_dst.channel_group_find("Gröpje");
ASSERT_NE(nullptr, group_dst) << "Expected channel group to be created";
ASSERT_EQ(group_dst, fcurve_to_move.grp) << "Expected group membership to move as well";
}
} // namespace blender::animrig::tests