Refactor: Anim: add function to ensure an F-Curve exists in legacy Action

Split the legacy Action handling code of `action_fcurve_ensure()` into a
new function `action_fcurve_ensure_legacy()`. This makes it possible for
unit tests to explicitly create a legacy Action for testing, regardless
of the 'Slotted Actions' experimental feature flag.

When we drop that flag, the unit tests that explicitly test legacy
behaviour will keep running.

Pull Request: https://projects.blender.org/blender/blender/pulls/128892
This commit is contained in:
Sybren A. Stüvel
2024-10-11 13:07:20 +02:00
parent f4fc683fc4
commit b7049a7c04
5 changed files with 140 additions and 61 deletions

View File

@@ -1342,10 +1342,8 @@ Span<const FCurve *> fcurves_for_action_slot(const Action &action, slot_handle_t
* previously-unanimated depsgraph component may become animated now.
*
* \param ptr: RNA pointer for the struct the fcurve is being looked up/created
* for. For legacy actions this is optional and may be null, but if provided is
* used to do things like set the fcurve color properly. For layered actions
* this parameter is required, and is used to create and assign an appropriate
* slot if needed when creating the fcurve.
* for. It is used to create and assign an appropriate slot if needed when
* creating the fcurve, and set the fcurve color properly
*
* \param fcurve_descriptor: description of the fcurve to lookup/create. Note
* that this is *not* relative to `ptr` (e.g. if `ptr` is not an ID). It should
@@ -1357,6 +1355,21 @@ FCurve *action_fcurve_ensure(Main *bmain,
PointerRNA *ptr,
FCurveDescriptor fcurve_descriptor);
/**
* Same as above, but creates a legacy Action.
*
* \note this function should ONLY be used in unit tests, in order to create
* legacy Actions for testing. Or in the very rare cases where handling of
* legacy Actions is still necessary AND you have no PointerRNA. In all other
* cases, just call #action_fcurve_ensure, it'll do the right thing
* transparently on whatever Action you give it.
*/
FCurve *action_fcurve_ensure_legacy(Main *bmain,
bAction *act,
const char group[],
PointerRNA *ptr,
FCurveDescriptor fcurve_descriptor);
/**
* Find the F-Curve in the given Action.
*

View File

@@ -2394,50 +2394,65 @@ FCurve *action_fcurve_ensure(Main *bmain,
return nullptr;
}
if (!animrig::legacy::action_treat_as_legacy(*act)) {
/* NOTE: for layered actions we require the following:
*
* - `ptr` is non-null.
* - `ptr` has an `owner_id` that already uses `act`.
*
* This isn't for any principled reason, but rather is because adding
* support for layered actions to this function was a fix to make Follow
* Path animation work properly with layered actions (see PR #124353), and
* those are the requirements the Follow Path code conveniently met.
* Moreover those requirements were also already met by the other call sites
* that potentially call this function with layered actions.
*
* Trying to puzzle out what "should" happen when these requirements don't
* hold, or if this is even the best place to handle the layered action
* cases at all, was leading to discussion of larger changes than made sense
* to tackle at that point. */
Action &action = act->wrap();
BLI_assert(ptr != nullptr);
if (ptr == nullptr || ptr->owner_id == nullptr) {
return nullptr;
}
ID &animated_id = *ptr->owner_id;
BLI_assert(get_action(animated_id) == &action);
if (get_action(animated_id) != &action) {
return nullptr;
}
/* Ensure the id has an assigned slot. */
Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
if (!slot) {
/* This means the ID type is not animatable. */
return nullptr;
}
action.layer_keystrip_ensure();
assert_baklava_phase_1_invariants(action);
StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
return &strip_data.channelbag_for_slot_ensure(*slot).fcurve_ensure(bmain, fcurve_descriptor);
if (animrig::legacy::action_treat_as_legacy(*act)) {
return action_fcurve_ensure_legacy(bmain, act, group, ptr, fcurve_descriptor);
}
/* NOTE: for layered actions we require the following:
*
* - `ptr` is non-null.
* - `ptr` has an `owner_id` that already uses `act`.
*
* This isn't for any principled reason, but rather is because adding
* support for layered actions to this function was a fix to make Follow
* Path animation work properly with layered actions (see PR #124353), and
* those are the requirements the Follow Path code conveniently met.
* Moreover those requirements were also already met by the other call sites
* that potentially call this function with layered actions.
*
* Trying to puzzle out what "should" happen when these requirements don't
* hold, or if this is even the best place to handle the layered action
* cases at all, was leading to discussion of larger changes than made sense
* to tackle at that point. */
Action &action = act->wrap();
BLI_assert(ptr != nullptr);
if (ptr == nullptr || ptr->owner_id == nullptr) {
return nullptr;
}
ID &animated_id = *ptr->owner_id;
BLI_assert(get_action(animated_id) == &action);
if (get_action(animated_id) != &action) {
return nullptr;
}
/* Ensure the id has an assigned slot. */
Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
if (!slot) {
/* This means the ID type is not animatable. */
return nullptr;
}
action.layer_keystrip_ensure();
assert_baklava_phase_1_invariants(action);
StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
return &strip_data.channelbag_for_slot_ensure(*slot).fcurve_ensure(bmain, fcurve_descriptor);
}
FCurve *action_fcurve_ensure_legacy(Main *bmain,
bAction *act,
const char group[],
PointerRNA *ptr,
FCurveDescriptor fcurve_descriptor)
{
if (!act) {
return nullptr;
}
BLI_assert(act->wrap().is_empty() || act->wrap().is_action_legacy());
/* Try to find f-curve matching for this setting.
* - add if not found and allowed to add one
* TODO: add auto-grouping support? how this works will need to be resolved
@@ -2463,7 +2478,7 @@ FCurve *action_fcurve_ensure(Main *bmain,
BLI_assert_msg(!fcurve_descriptor.prop_subtype.has_value(),
"Did not expect a prop_subtype to be passed in. This is fine, but does need some "
"changes to action_fcurve_ensure() to deal with it");
"changes to action_fcurve_ensure_legacy() to deal with it");
fcu = create_fcurve_for_channel(
{fcurve_descriptor.rna_path, fcurve_descriptor.array_index, prop_subtype});

View File

@@ -823,8 +823,8 @@ TEST_F(ActionLayersTest, action_slot_get_id_for_keying__empty_action)
/* Double-check that the action is considered empty for the test. */
EXPECT_TRUE(action->is_empty());
/* A `primary_id` that uses the action should get returned. Every other case
* should return nullptr. */
/* 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(&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));
@@ -832,7 +832,7 @@ TEST_F(ActionLayersTest, action_slot_get_id_for_keying__empty_action)
TEST_F(ActionLayersTest, action_slot_get_id_for_keying__legacy_action)
{
FCurve *fcurve = action_fcurve_ensure(bmain, action, nullptr, nullptr, {"location", 0});
FCurve *fcurve = action_fcurve_ensure_legacy(bmain, action, nullptr, nullptr, {"location", 0});
EXPECT_FALSE(fcurve == nullptr);
EXPECT_TRUE(assign_action(action, cube->id));
@@ -877,8 +877,10 @@ TEST_F(ActionLayersTest, action_slot_get_id_for_keying__layered_action)
TEST_F(ActionLayersTest, conversion_to_layered)
{
EXPECT_TRUE(action->is_empty());
FCurve *legacy_fcu_0 = action_fcurve_ensure(bmain, action, "Test", nullptr, {"location", 0});
FCurve *legacy_fcu_1 = action_fcurve_ensure(bmain, action, "Test", nullptr, {"location", 1});
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;
@@ -910,7 +912,7 @@ TEST_F(ActionLayersTest, conversion_to_layered)
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(bmain, long_name_action, "Long", nullptr, {"location", 0});
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". */
@@ -921,11 +923,11 @@ TEST_F(ActionLayersTest, conversion_to_layered)
TEST_F(ActionLayersTest, conversion_to_layered_action_groups)
{
EXPECT_TRUE(action->is_empty());
action_fcurve_ensure(bmain, action, "Test", nullptr, {"location", 0});
action_fcurve_ensure(bmain, action, "Test", nullptr, {"rotation_euler", 1});
action_fcurve_ensure(bmain, action, "Test_Two", nullptr, {"scale", 1});
action_fcurve_ensure(bmain, action, "Test_Three", nullptr, {"show_name", 1});
action_fcurve_ensure(bmain, action, "Test_Rename", nullptr, {"show_axis", 1});
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);

View File

@@ -106,6 +106,7 @@ class KeyframingTest : public testing::Test {
object_with_nla = BKE_object_add_only_object(bmain, OB_EMPTY, "EmptyWithNLA");
object_with_nla_rna_pointer = RNA_id_pointer_create(&object_with_nla->id);
nla_action = static_cast<bAction *>(BKE_id_new(bmain, ID_AC, "NLAAction"));
this->ensure_action_is_legacy(*nla_action);
cube = BKE_object_add_only_object(bmain, OB_MESH, "cube");
cube_rna_pointer = RNA_id_pointer_create(&cube->id);
@@ -142,6 +143,40 @@ class KeyframingTest : public testing::Test {
{
BKE_main_free(bmain);
}
/**
* Create a legacy Action and assign it to the ID.
*
* Use of this function indicates that the unit test should be converted to
* use layered Actions.
*/
void ensure_legacy_action_assigned(ID &id)
{
AnimData *adt = BKE_animdata_from_id(&id);
BLI_assert(!adt || !adt->action);
UNUSED_VARS_NDEBUG(adt);
bAction &action = animrig::action_add(*bmain, "LegacyAction");
this->ensure_action_is_legacy(action);
const bool ok = assign_action(&action, id);
BLI_assert(ok);
UNUSED_VARS_NDEBUG(ok);
}
/**
* Add an F-Curve Group. This marks the Action as legacy. Since most tests are on F-Curves,
* adding a Group here is the less invasive way to make this a Legacy Action.
*
* Use of this function indicates the test should be converted to layered Actions.
*/
void ensure_action_is_legacy(bAction &action)
{
bActionGroup *new_group = static_cast<bActionGroup *>(
MEM_callocN(sizeof(bActionGroup), __func__));
STRNCPY(new_group->name, "Legacy Forcer");
BLI_addtail(&action.groups, new_group);
}
};
/* ------------------------------------------------------------
@@ -978,6 +1013,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__non_array_property)
/* First time should create the AnimData, Action, and FCurve with a single
* key. */
object->empty_drawsize = 42.0;
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1039,6 +1075,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__optional_frame)
AnimationEvalContext anim_eval_context = {nullptr, 5.0};
object->rotmode = ROT_MODE_XYZ;
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1075,6 +1112,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__optional_channel_group)
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* If the channel group is not explicitly passed, the default should be used. */
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result_1 = insert_keyframes(
bmain,
&object_rna_pointer,
@@ -1122,6 +1160,7 @@ TEST_F(KeyframingTest, insert_keyframes__single_element)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1143,6 +1182,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__all_elements)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1169,6 +1209,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__pose_bone_rna_pointer)
PointerRNA pose_bone_rna_pointer = RNA_pointer_create(
&armature_object->id, &RNA_PoseBone, pchan);
ensure_legacy_action_assigned(armature_object->id);
const CombinedKeyingResult result = insert_keyframes(bmain,
&pose_bone_rna_pointer,
std::nullopt,
@@ -1192,6 +1233,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__pose_bone_owner_id_point
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
ensure_legacy_action_assigned(armature_object->id);
const CombinedKeyingResult result = insert_keyframes(
bmain,
&armature_object_rna_pointer,
@@ -1216,6 +1258,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__multiple_properties)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result =
insert_keyframes(bmain,
@@ -1269,6 +1312,10 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__only_available)
* object without an action. */
ASSERT_EQ(nullptr, object->adt->action);
/* This will create & assign an Action, which is necessary for the
* insert_keyframes() function to take the 'legacy Action' code path. */
ensure_legacy_action_assigned(object->id);
/* Insert a key on two of the elements without using the flag so that there
* will be two fcurves. */
insert_keyframes(bmain,
@@ -1309,6 +1356,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__only_replace)
object->rot[0] = 42.0;
object->rot[1] = 42.0;
object->rot[2] = 42.0;
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1397,6 +1445,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__only_needed)
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First attempt should succeed, because there are no fcurves yet. */
ensure_legacy_action_assigned(object->id);
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
@@ -1528,7 +1577,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__quaternion_on_nla__only_
/* Create an fcurve and key for a single quaternion channel. */
PointerRNA id_rna_ptr = RNA_id_pointer_create(&object_with_nla->id);
FCurve *fcu = action_fcurve_ensure(
FCurve *fcu = action_fcurve_ensure_legacy(
bmain, nla_action, nullptr, &id_rna_ptr, {"rotation_quaternion", 0});
const KeyframeSettings keyframe_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO_ANIM, BEZT_IPO_BEZ};
insert_vert_fcurve(fcu, {1.0, 1.0}, keyframe_settings, INSERTKEY_NOFLAGS);
@@ -1568,7 +1617,7 @@ TEST_F(KeyframingTest, insert_keyframes__legacy_action__quaternion_on_nla__only_
/* Directly create an fcurve and key for a single quaternion channel. */
PointerRNA id_rna_ptr = RNA_id_pointer_create(&object_with_nla->id);
FCurve *fcu = action_fcurve_ensure(
FCurve *fcu = action_fcurve_ensure_legacy(
bmain, nla_action, nullptr, &id_rna_ptr, {"rotation_quaternion", 0});
const KeyframeSettings keyframe_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO_ANIM, BEZT_IPO_BEZ};
insert_vert_fcurve(fcu, {11.0, 1.0}, keyframe_settings, INSERTKEY_NOFLAGS);

View File

@@ -1127,7 +1127,7 @@ static FCurve *rna_Action_fcurve_new(bAction *act,
act->id.name + 2);
return nullptr;
}
return blender::animrig::action_fcurve_ensure(
return blender::animrig::action_fcurve_ensure_legacy(
bmain,
act,
fcurve_descriptor.channel_group ? fcurve_descriptor.channel_group->c_str() : nullptr,