Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BKE_anim_data.hh"
#include "BKE_nla.hh"
#include "DNA_constraint_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
namespace blender::animrig {
void foreach_fcurve_in_action(Action &action, FunctionRef<void(FCurve &fcurve)> callback)
{
for (Layer *layer : action.layers()) {
for (Strip *strip : layer->strips()) {
if (strip->type() != Strip::Type::Keyframe) {
continue;
}
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
for (FCurve *fcu : bag->fcurves()) {
callback(*fcu);
}
}
}
}
}
void foreach_fcurve_in_action_slot_editable(Action &action,
slot_handle_t handle,
FunctionRef<void(FCurve &fcurve)> callback)
{
/* Once layers can be locked, this needs to be checked here. */
assert_baklava_phase_1_invariants(action);
for (Layer *layer : action.layers()) {
for (Strip *strip : layer->strips()) {
if (strip->type() != Strip::Type::Keyframe) {
continue;
}
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
if (bag->slot_handle != handle) {
continue;
}
for (FCurve *fcu : bag->fcurves()) {
BLI_assert(fcu != nullptr);
if (fcu->flag & FCURVE_PROTECTED) {
continue;
}
callback(*fcu);
}
}
}
}
}
void foreach_fcurve_in_action_slot(Action &action,
slot_handle_t handle,
FunctionRef<void(FCurve &fcurve)> callback)
{
for (Layer *layer : action.layers()) {
for (Strip *strip : layer->strips()) {
if (strip->type() != Strip::Type::Keyframe) {
continue;
}
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
if (bag->slot_handle != handle) {
continue;
}
for (FCurve *fcu : bag->fcurves()) {
BLI_assert(fcu != nullptr);
callback(*fcu);
}
}
}
}
}
bool foreach_action_slot_use(
const ID &animated_id,
FunctionRef<bool(const Action &action, slot_handle_t slot_handle)> callback)
{
const auto forward_to_callback = [&](ID & /* animated_id */,
bAction *&action_ptr_ref,
const slot_handle_t &slot_handle_ref,
char * /*last_slot_identifier*/) -> bool {
if (!action_ptr_ref) {
return true;
}
return callback(const_cast<const Action &>(action_ptr_ref->wrap()), slot_handle_ref);
};
return foreach_action_slot_use_with_references(const_cast<ID &>(animated_id),
forward_to_callback);
}
bool foreach_action_slot_use_with_references(
ID &animated_id,
FunctionRef<bool(ID &animated_id,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *last_slot_identifier)> callback)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (adt) {
if (adt->action) {
/* Direct assignment. */
if (!callback(animated_id, adt->action, adt->slot_handle, adt->last_slot_identifier)) {
return false;
}
}
/* NLA strips. */
const bool looped_until_last_strip = bke::nla::foreach_strip_adt(*adt, [&](NlaStrip *strip) {
if (strip->act) {
if (!callback(
animated_id, strip->act, strip->action_slot_handle, strip->last_slot_identifier))
{
return false;
}
}
return true;
});
if (!looped_until_last_strip) {
return false;
}
}
/* The rest of the code deals with constraints, so only relevant when this is an Object. */
if (GS(animated_id.name) != ID_OB) {
return true;
}
const Object &object = reinterpret_cast<const Object &>(animated_id);
/**
* Visit a constraint, and call the callback if it's an Action constraint.
*
* \returns whether to continue looping over possible uses of Actions, i.e.
* the return value of the callback.
*/
auto visit_constraint = [&](const bConstraint &constraint) -> bool {
if (constraint.type != CONSTRAINT_TYPE_ACTION) {
return true;
}
bActionConstraint *constraint_data = static_cast<bActionConstraint *>(constraint.data);
if (!constraint_data->act) {
return true;
}
return callback(animated_id,
constraint_data->act,
constraint_data->action_slot_handle,
constraint_data->last_slot_identifier);
};
/* Visit Object constraints. */
for (bConstraint &con : object.constraints) {
if (!visit_constraint(con)) {
return false;
}
}
/* Visit Pose Bone constraints. */
if (object.type == OB_ARMATURE) {
for (bPoseChannel &pchan : object.pose->chanbase) {
for (bConstraint &con : pchan.constraints) {
if (!visit_constraint(con)) {
return false;
}
}
}
}
return true;
}
bool foreach_action_slot_use_with_rna(ID &animated_id,
FunctionRef<bool(ID &animated_id,
bAction *action,
PointerRNA &action_slot_ptr,
PropertyRNA &action_slot_prop,
char *last_slot_identifier)> callback)
{
/* This function has to copy the logic of #foreach_action_slot_use_with_references(),
* as it needs to know where exactly those pointers came from. */
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (adt) {
if (adt->action) {
/* Direct assignment. */
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_AnimData, adt);
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
if (!callback(animated_id, adt->action, ptr, *prop, adt->last_slot_identifier)) {
return false;
}
}
/* NLA strips. */
const bool looped_until_last_strip = bke::nla::foreach_strip_adt(*adt, [&](NlaStrip *strip) {
if (strip->act) {
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_NlaStrip, strip);
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
if (!callback(animated_id, strip->act, ptr, *prop, strip->last_slot_identifier)) {
return false;
}
}
return true;
});
if (!looped_until_last_strip) {
return false;
}
}
/* The rest of the code deals with constraints, so only relevant when this is an Object. */
if (GS(animated_id.name) != ID_OB) {
return true;
}
const Object &object = reinterpret_cast<const Object &>(animated_id);
/**
* Visit a constraint, and call the callback if it's an Action constraint.
*
* \returns whether to continue looping over possible uses of Actions, i.e.
* the return value of the callback.
*/
auto visit_constraint = [&](bConstraint &constraint) -> bool {
if (constraint.type != CONSTRAINT_TYPE_ACTION) {
return true;
}
bActionConstraint *constraint_data = static_cast<bActionConstraint *>(constraint.data);
if (!constraint_data->act) {
return true;
}
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_ActionConstraint, &constraint);
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
return callback(
animated_id, constraint_data->act, ptr, *prop, constraint_data->last_slot_identifier);
};
/* Visit Object constraints. */
for (bConstraint &con : object.constraints) {
if (!visit_constraint(con)) {
return false;
}
}
/* Visit Pose Bone constraints. */
if (object.type == OB_ARMATURE) {
for (bPoseChannel &pchan : object.pose->chanbase) {
for (bConstraint &con : pchan.constraints) {
if (!visit_constraint(con)) {
return false;
}
}
}
}
return true;
}
} // namespace blender::animrig

View File

@@ -0,0 +1,178 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "testing/testing.h"
namespace blender::animrig::tests {
class ActionIteratorsTest : public bke::BlenderGTestBase {
public:
Main *bmain;
Action *action;
void SetUp() override
{
bmain = BKE_main_new();
action = BKE_id_new<Action>(bmain, "ACLayeredAction");
}
void TearDown() override
{
BKE_main_free(bmain);
}
};
TEST_F(ActionIteratorsTest, iterate_all_fcurves_of_slot)
{
Slot &cube_slot = action->slot_add();
Slot &monkey_slot = action->slot_add();
/* Try iterating an empty action. */
Vector<const FCurve *> no_fcurves;
foreach_fcurve_in_action_slot(
*action, cube_slot.handle, [&](const FCurve &fcurve) { no_fcurves.append(&fcurve); });
ASSERT_TRUE(no_fcurves.is_empty());
Layer &layer = action->layer_add("Layer One");
Strip &strip = layer.strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
const KeyframeSettings settings = get_keyframe_settings(false);
/* Insert 3 FCurves for each slot. */
for (int i = 0; i < 3; i++) {
SingleKeyingResult result_cube = strip_data.keyframe_insert(
bmain, cube_slot, {"location", i}, {1.0f, 0.0f}, settings);
ASSERT_EQ(SingleKeyingResult::SUCCESS, result_cube)
<< "Expected keyframe insertion to be successful";
SingleKeyingResult result_monkey = strip_data.keyframe_insert(
bmain, monkey_slot, {"rotation", i}, {1.0f, 0.0f}, settings);
ASSERT_EQ(SingleKeyingResult::SUCCESS, result_monkey)
<< "Expected keyframe insertion to be successful";
}
/* Get all FCurves. */
Vector<const FCurve *> cube_fcurves;
foreach_fcurve_in_action_slot(
*action, cube_slot.handle, [&](const FCurve &fcurve) { cube_fcurves.append(&fcurve); });
ASSERT_EQ(cube_fcurves.size(), 3);
for (const FCurve *fcurve : cube_fcurves) {
ASSERT_STREQ(fcurve->rna_path, "location");
}
/* Get only FCurves with index 0 which should be 1. */
Vector<const FCurve *> monkey_fcurves;
foreach_fcurve_in_action_slot(*action, monkey_slot.handle, [&](const FCurve &fcurve) {
if (fcurve.array_index == 0) {
monkey_fcurves.append(&fcurve);
}
});
ASSERT_EQ(monkey_fcurves.size(), 1);
ASSERT_STREQ(monkey_fcurves[0]->rna_path, "rotation");
/* Slots handles are just numbers. Passing in a slot handle that doesn't exist should return
* nothing. */
Vector<const FCurve *> invalid_slot_fcurves;
foreach_fcurve_in_action_slot(
*action, monkey_slot.handle + cube_slot.handle, [&](const FCurve &fcurve) {
invalid_slot_fcurves.append(&fcurve);
});
ASSERT_TRUE(invalid_slot_fcurves.is_empty());
}
TEST_F(ActionIteratorsTest, foreach_action_slot_use_with_references)
{
/* Create a cube and assign the Action + a slot. */
Object *cube = BKE_id_new<Object>(bmain, "OBCube");
Slot *slot_cube = assign_action_ensure_slot_for_keying(*action, cube->id);
ASSERT_NE(slot_cube, nullptr);
/* Create another Action with slot to assign. */
Action &other_action = BKE_id_new<bAction>(bmain, "ACAnotherAction")->wrap();
Slot &another_slot = other_action.slot_add();
std::optional<ActionSlotAssignmentResult> slot_assignment_result;
bool all_assigns_ok = true;
const auto assign_other_action = [&](ID & /* animated_id */,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *last_slot_identifier) -> bool {
/* Assign the other Action. */
all_assigns_ok &= generic_assign_action(
cube->id, &other_action, action_ptr_ref, slot_handle_ref, last_slot_identifier);
/* Assign the slot of the other Action. */
slot_assignment_result = generic_assign_action_slot(
&another_slot, cube->id, action_ptr_ref, slot_handle_ref, last_slot_identifier);
return true;
};
foreach_action_slot_use_with_references(cube->id, assign_other_action);
ASSERT_TRUE(all_assigns_ok);
/* Check the result, the slot assignment should have been changed. */
ASSERT_TRUE(slot_assignment_result.has_value());
EXPECT_EQ(ActionSlotAssignmentResult::OK, slot_assignment_result.value());
std::optional<std::pair<Action *, Slot *>> action_and_slot = get_action_slot_pair(cube->id);
ASSERT_TRUE(action_and_slot.has_value());
EXPECT_EQ(&other_action, action_and_slot->first)
<< "Expected Action " << other_action.id.name << " but found "
<< action_and_slot->first->id.name;
EXPECT_EQ(&another_slot, action_and_slot->second)
<< "Expected Slot " << another_slot.identifier << " but found "
<< action_and_slot->second->identifier;
}
TEST_F(ActionIteratorsTest, foreach_action_slot_use_with_rna)
{
/* Create a cube and assign the Action + a slot. */
Object *cube = BKE_id_new<Object>(bmain, "OBCube");
Slot *slot_cube = assign_action_ensure_slot_for_keying(*action, cube->id);
ASSERT_NE(slot_cube, nullptr);
Slot &another_slot = action->slot_add();
const auto assign_other_slot = [&](ID & /* animated_id */,
bAction *action,
PointerRNA &action_slot_owner_ptr,
PropertyRNA &action_slot_prop,
char * /*last_slot_identifier*/) -> bool {
PointerRNA rna_slot = RNA_pointer_create_discrete(&action->id, RNA_ActionSlot, &another_slot);
RNA_property_pointer_set(&action_slot_owner_ptr, &action_slot_prop, rna_slot, nullptr);
return true;
};
foreach_action_slot_use_with_rna(cube->id, assign_other_slot);
/* Check the result, the slot assignment should have been changed. */
std::optional<std::pair<Action *, Slot *>> action_and_slot = get_action_slot_pair(cube->id);
ASSERT_TRUE(action_and_slot.has_value());
EXPECT_EQ(action, action_and_slot->first)
<< "Expected Action " << action->id.name << " but found " << action_and_slot->first->id.name;
EXPECT_EQ(&another_slot, action_and_slot->second)
<< "Expected Slot " << another_slot.identifier << " but found "
<< action_and_slot->second->identifier;
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_action_legacy.hh"
#include "BLI_listbase_wrapper.hh"
#include "BKE_fcurve.hh"
#include "BLT_translation.hh"
namespace blender::animrig::legacy {
/* Lots of template args to support transparent non-const and const versions. */
template<typename ActionType,
typename FCurveType,
typename LayerType,
typename StripType,
typename StripKeyframeDataType,
typename ChannelbagType>
static Vector<FCurveType *> fcurves_all_templated(ActionType &action)
{
Vector<FCurveType *> all_fcurves;
for (LayerType *layer : action.layers()) {
for (StripType *strip : layer->strips()) {
switch (strip->type()) {
case Strip::Type::Keyframe: {
StripKeyframeDataType &strip_data = strip->template data<StripKeyframeData>(action);
for (ChannelbagType *bag : strip_data.channelbags()) {
for (FCurveType *fcurve : bag->fcurves()) {
all_fcurves.append(fcurve);
}
}
}
}
}
}
return all_fcurves;
}
Vector<FCurve *> fcurves_all(bAction *action)
{
if (!action) {
return {};
}
return fcurves_all_templated<Action, FCurve, Layer, Strip, StripKeyframeData, Channelbag>(
action->wrap());
}
Vector<const FCurve *> fcurves_all(const bAction *action)
{
if (!action) {
return {};
}
return fcurves_all_templated<const Action,
const FCurve,
const Layer,
const Strip,
const StripKeyframeData,
const Channelbag>(action->wrap());
}
/* Lots of template args to support transparent non-const and const versions. */
template<typename ActionType,
typename FCurveType,
typename LayerType,
typename StripType,
typename StripKeyframeDataType,
typename ChannelbagType>
static Vector<FCurveType *> fcurves_for_action_slot_templated(ActionType &action,
const slot_handle_t slot_handle)
{
Vector<FCurveType *> as_vector(animrig::fcurves_for_action_slot(action, slot_handle));
return as_vector;
}
bool assigned_action_has_keyframes(AnimData *adt)
{
if (adt == nullptr || adt->action == nullptr) {
return false;
}
Action &action = adt->action->wrap();
return action.has_keyframes(adt->slot_handle);
}
Vector<bActionGroup *> channel_groups_all(bAction *action)
{
if (!action) {
return {};
}
Action &action_wrap = action->wrap();
Vector<bActionGroup *> all_groups;
for (Layer *layer : action_wrap.layers()) {
for (Strip *strip : layer->strips()) {
switch (strip->type()) {
case Strip::Type::Keyframe: {
StripKeyframeData &strip_data = strip->template data<StripKeyframeData>(action_wrap);
for (Channelbag *bag : strip_data.channelbags()) {
all_groups.extend(bag->channel_groups());
}
}
}
}
}
return all_groups;
}
Vector<bActionGroup *> channel_groups_for_assigned_slot(AnimData *adt)
{
if (!adt || !adt->action) {
return {};
}
Action &action = adt->action->wrap();
Channelbag *bag = channelbag_for_action_slot(action, adt->slot_handle);
if (!bag) {
return {};
}
Vector<bActionGroup *> slot_groups(bag->channel_groups());
return slot_groups;
}
bool action_fcurves_remove(bAction &action,
const slot_handle_t slot_handle,
const StringRefNull rna_path_prefix)
{
BLI_assert(!rna_path_prefix.is_empty());
if (rna_path_prefix.is_empty()) {
return false;
}
Channelbag *bag = channelbag_for_action_slot(action.wrap(), slot_handle);
if (!bag) {
return false;
}
bool any_removed = false;
for (int64_t fcurve_index = 0; fcurve_index < bag->fcurve_array_num; fcurve_index++) {
FCurve *fcurve = bag->fcurve(fcurve_index);
if (!fcurve->rna_path) {
continue;
}
if (STRPREFIX(fcurve->rna_path, rna_path_prefix.c_str())) {
bag->fcurve_remove_by_index(fcurve_index);
fcurve_index--;
any_removed = true;
}
}
return any_removed;
}
} // namespace blender::animrig::legacy

View File

@@ -0,0 +1,116 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_action_legacy.hh"
#include "BKE_fcurve.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "BLI_listbase.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
class ActionLegacyTest : public bke::BlenderGTestBase {
public:
Main *bmain;
void SetUp() override
{
bmain = BKE_main_new();
}
void TearDown() override
{
BKE_main_free(bmain);
}
bAction *create_empty_action()
{
return BKE_id_new<bAction>(bmain, "ACAction");
}
FCurve *fcurve_add_legacy(bAction *action, const StringRefNull rna_path, const int array_index)
{
FCurve *fcurve = MEM_new<FCurve>(__func__);
BKE_fcurve_rnapath_set(*fcurve, rna_path);
fcurve->array_index = array_index;
BLI_addtail(&action->curves, fcurve);
return fcurve;
}
};
TEST_F(ActionLegacyTest, fcurves_all)
{
{ /* nil pointer. */
bAction *action = nullptr;
Vector<FCurve *> fcurves = legacy::fcurves_all(action);
EXPECT_TRUE(fcurves.is_empty());
}
{ /* Empty Action. */
Vector<FCurve *> fcurves = legacy::fcurves_all(create_empty_action());
EXPECT_TRUE(fcurves.is_empty());
}
Action &action = create_empty_action()->wrap();
Slot &slot1 = action.slot_add();
Slot &slot2 = action.slot_add();
action.layer_keystrip_ensure();
StripKeyframeData &key_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
FCurve &fcurve1 = key_data.channelbag_for_slot_ensure(slot1).fcurve_ensure(bmain,
{"location", 1});
FCurve &fcurve2 = key_data.channelbag_for_slot_ensure(slot2).fcurve_ensure(bmain, {"scale", 2});
Vector<FCurve *> fcurves_expect = {&fcurve1, &fcurve2};
EXPECT_EQ(fcurves_expect, legacy::fcurves_all(&action));
}
TEST_F(ActionLegacyTest, action_fcurves_remove)
{
{ /* Empty Action. */
bAction *action = create_empty_action();
EXPECT_FALSE(legacy::action_fcurves_remove(*action, Slot::unassigned, "rotation"));
}
/* Create an Action with two slots, to check that the 2nd slot is not affected
* by removal from the 1st. */
Action &action = create_empty_action()->wrap();
Slot &slot_1 = action.slot_add();
Slot &slot_2 = action.slot_add();
action.layer_keystrip_ensure();
StripKeyframeData *strip_data = action.strip_keyframe_data()[0];
Channelbag &bag_1 = strip_data->channelbag_for_slot_ensure(slot_1);
Channelbag &bag_2 = strip_data->channelbag_for_slot_ensure(slot_2);
/* Add some F-Curves to each channelbag. */
FCurve &fcurve_loc_x = bag_1.fcurve_ensure(nullptr, {"location", 0});
bag_1.fcurve_ensure(nullptr, {"rotation_euler", 2});
bag_1.fcurve_ensure(nullptr, {"rotation_mode", 0});
FCurve &fcurve_loc_y = bag_1.fcurve_ensure(nullptr, {"location", 1});
bag_2.fcurve_ensure(nullptr, {"location", 0});
bag_2.fcurve_ensure(nullptr, {"rotation_euler", 2});
bag_2.fcurve_ensure(nullptr, {"rotation_mode", 0});
bag_2.fcurve_ensure(nullptr, {"location", 1});
/* Check that removing from slot_1 works as expected. */
EXPECT_TRUE(legacy::action_fcurves_remove(action, slot_1.handle, "rotation"));
Vector<FCurve *> fcurves_bag_1_expect = {&fcurve_loc_x, &fcurve_loc_y};
EXPECT_EQ(fcurves_bag_1_expect.as_span(),
animrig::fcurves_for_action_slot(action, slot_1.handle));
EXPECT_EQ(4, bag_2.fcurves().size())
<< "Expected all F-Curves for slot 2 to be there after manipulating slot 1";
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Internal C++ functions to deal with Actions, Slots, and their runtime data.
*/
#include "BKE_anim_data.hh"
#include "BKE_global.hh"
#include "BKE_lib_query.hh"
#include "BKE_main.hh"
#include "BKE_nla.hh"
#include "BKE_node.hh"
#include "BLI_listbase.h"
#include "BLI_set.hh"
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "action_runtime.hh"
namespace blender::animrig::internal {
void rebuild_slot_user_cache(Main &bmain)
{
/* Loop over all Actions and clear their slots' user cache. */
for (bAction &dna_action : bmain.actions) {
Action &action = dna_action.wrap();
for (Slot *slot : action.slots()) {
BLI_assert_msg(slot->runtime, "Slot::runtime should always be allocated");
slot->runtime->users.clear();
}
}
/* Mark all Slots as clear. This is a bit of a lie, because the code below still has to run.
* However, this is a necessity to make the `slot.users_add(*id)` call work without triggering
* an infinite recursion.
*
* The alternative would be to go around the `slot.users_add()` function and access the
* runtime directly, but this is IMO a bit cleaner. */
bmain.is_action_slot_to_id_map_dirty = false;
/* Visit any ID to see which Action+Slot it is using. Returns whether the ID
* was visited for the first time. */
Set<ID *> visited_ids;
auto visit_id = [&visited_ids](ID *id) -> bool {
BLI_assert(id);
if (!visited_ids.add(id)) {
return false;
}
foreach_action_slot_use(*id, [&](const Action &action, slot_handle_t slot_handle) {
const Slot *slot = action.slot_for_handle(slot_handle);
if (!slot) {
return true;
}
/* Constant cast because the `foreach` produces const Actions, and I (Sybren)
* didn't want to make a non-const duplicate. */
const_cast<Slot *>(slot)->users_add(*id);
return true;
});
return true;
};
/* Loop over all IDs to cache their slot usage. */
ListBaseT<ID> *ids_of_idtype;
ID *id;
FOREACH_MAIN_LISTBASE_BEGIN (&bmain, ids_of_idtype) {
/* Check whether this ID type can be animated. If not, just skip all IDs of this type. */
id = static_cast<ID *>(ids_of_idtype->first);
if (!id || !id_type_can_have_animdata(GS(id->name))) {
continue;
}
FOREACH_MAIN_LISTBASE_ID_BEGIN (ids_of_idtype, id) {
BLI_assert(id_can_have_animdata(id));
/* Process the ID itself. */
if (!visit_id(id)) {
continue;
}
/* Process embedded IDs, as these are not listed in bmain, but still can
* have their own Action+Slot. Unfortunately there is no generic looper
* for embedded IDs. At this moment the only animatable embedded ID is a
* node tree. */
bNodeTree *node_tree = bke::node_tree_from_id(id);
if (node_tree) {
visit_id(&node_tree->id);
}
}
FOREACH_MAIN_LISTBASE_ID_END;
}
FOREACH_MAIN_LISTBASE_END;
}
} // namespace blender::animrig::internal

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Internal C++ functions to deal with Actions, Slots, and their runtime data.
*/
#pragma once
#include "BLI_vector.hh"
namespace blender {
struct ID;
struct Main;
namespace animrig {
/**
* Not placed in the 'internal' namespace, as this type is forward-declared in
* DNA_action_types.h, and that shouldn't reference the internal namespace.
*/
class SlotRuntime {
public:
/**
* Cache of pointers to the IDs that are animated by this slot.
*
* Note that this is a vector for simplicity, as the majority of the slots
* will have zero or one user. Semantically it's treated as a set: order
* doesn't matter, and it has no duplicate entries.
*
* \note This is NOT thread-safe.
*/
Vector<ID *> users;
};
namespace internal {
/**
* Rebuild the #SlotRuntime::users cache of all Slots in all Action for a specific `bmain`.
*
* The reason that all slot users are re-cached at once is two-fold:
*
* 1. Regardless of how many slot caches are rebuilt, this function will need
* to loop over all IDs anyway.
* 2. Deletion of IDs may be hard to detect otherwise. This is a bit of a weak
* argument, as if this is not implemented properly (i.e. not un-assigning
* the Action first), the 'dirty' flag will also not be set, and thus a
* rebuild will not be triggered. In any case, because the rebuild is global,
* any subsequent call at least ensures correctness even with such bugs.
*/
void rebuild_slot_user_cache(Main &bmain);
} // namespace internal
} // namespace animrig
} // namespace blender

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "DNA_action_types.h"
#include "DNA_anim_types.h"
#include "BLI_set.hh"
#include "BKE_fcurve.hh"
#include "ANIM_action.hh"
#include "ANIM_action_legacy.hh"
namespace blender::animrig {
void action_deselect_keys(Action &action)
{
for (FCurve *fcu : legacy::fcurves_all(&action)) {
BKE_fcurve_deselect_all_keys(*fcu);
}
}
void deselect_keys_actions(Span<bAction *> actions)
{
Set<bAction *> visited_actions;
for (bAction *action : actions) {
if (!visited_actions.add(action)) {
continue;
}
action_deselect_keys(action->wrap());
}
}
} // namespace blender::animrig

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,345 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include <fmt/format.h>
#include "ANIM_rna.hh"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_string.h"
#include "BLI_vector.hh"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"
namespace blender::animrig {
Vector<float> get_rna_values(PointerRNA *ptr, PropertyRNA *prop)
{
Vector<float> values;
if (RNA_property_array_check(prop)) {
const int length = RNA_property_array_length(ptr, prop);
switch (RNA_property_type(prop)) {
case PROP_BOOLEAN: {
bool *tmp_bool = MEM_new_array_uninitialized<bool>(length, __func__);
RNA_property_boolean_get_array(ptr, prop, tmp_bool);
for (int i = 0; i < length; i++) {
values.append(float(tmp_bool[i]));
}
MEM_delete(tmp_bool);
break;
}
case PROP_INT: {
int *tmp_int = MEM_new_array_uninitialized<int>(length, __func__);
RNA_property_int_get_array(ptr, prop, tmp_int);
for (int i = 0; i < length; i++) {
values.append(float(tmp_int[i]));
}
MEM_delete(tmp_int);
break;
}
case PROP_FLOAT: {
values.reinitialize(length);
RNA_property_float_get_array(ptr, prop, values.data());
break;
}
default:
values.reinitialize(length);
break;
}
}
else {
switch (RNA_property_type(prop)) {
case PROP_BOOLEAN:
values.append(float(RNA_property_boolean_get(ptr, prop)));
break;
case PROP_INT:
values.append(float(RNA_property_int_get(ptr, prop)));
break;
case PROP_FLOAT:
values.append(RNA_property_float_get(ptr, prop));
break;
case PROP_ENUM:
values.append(float(RNA_property_enum_get(ptr, prop)));
break;
default:
values.append(0.0f);
}
}
return values;
}
constexpr const char *pose_bone_path_prefix = "pose.bones[\"";
constexpr int pose_bone_path_prefix_length = std::char_traits<char>::length(pose_bone_path_prefix);
std::string get_pose_bone_rna_path(const bPoseChannel &pose_bone)
{
char name_esc[sizeof(pose_bone.name) * 2];
BLI_str_escape(name_esc, pose_bone.name, sizeof(name_esc));
return fmt::format("{}{}\"]", pose_bone_path_prefix, name_esc);
}
std::optional<std::string> pose_bone_name_from_rna_path(const StringRefNull rna_path)
{
if (rna_path.size() < pose_bone_path_prefix_length ||
!rna_path.startswith(pose_bone_path_prefix))
{
return std::nullopt;
}
const char *name_esc = rna_path.data() + pose_bone_path_prefix_length;
const char *name_esc_end = BLI_str_escape_find_quote(name_esc);
if (!name_esc_end) {
return std::nullopt;
}
char name[MAXBONENAME];
const size_t name_esc_len = size_t(name_esc_end - name_esc);
if (name_esc_len >= sizeof(name)) {
return std::nullopt;
}
BLI_str_unescape(name, name_esc, name_esc_len);
return name;
}
StringRefNull get_rotation_mode_path(const eRotationModes rotation_mode)
{
switch (rotation_mode) {
case ROT_MODE_QUAT:
return "rotation_quaternion";
case ROT_MODE_AXISANGLE:
return "rotation_axis_angle";
default:
return "rotation_euler";
}
}
std::optional<eRotationModes> get_rotation_mode_from_path(const StringRefNull rna_path)
{
/* Accounting for the difference between objects and bones where the latter is e.g.
* `pose.bones["foo"].rotation_euler`. Assumes that rfind returns -1 if the string
* is not found. */
const int start_of_propname = rna_path.rfind(".") + 1;
if (!rna_path.substr(start_of_propname, rna_path.size()).startswith("rotation_")) {
return std::nullopt;
}
/* We already know that "rotation_" is in the rna_path, we can skip the full check for
* "rotation_quaternion", "rotation_euler" or "rotation_axis_angle". */
if (rna_path.endswith("quaternion")) {
return ROT_MODE_QUAT;
}
else if (rna_path.endswith("euler")) {
/* Cannot determine the rotation order from the path alone. */
return ROT_MODE_EUL;
}
else if (rna_path.endswith("axis_angle")) {
return ROT_MODE_AXISANGLE;
}
return std::nullopt;
}
std::optional<eRotationModes> get_rotation_mode_from_rna_pointer(const PointerRNA &ptr)
{
if (ptr.type == RNA_PoseBone) {
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr.data);
return eRotationModes(pchan->rotmode);
}
if (ptr.type == RNA_Object) {
Object *ob = static_cast<Object *>(ptr.data);
return eRotationModes(ob->rotmode);
}
return std::nullopt;
}
bool is_rotation_path(const StringRefNull rna_path)
{
return get_rotation_mode_from_path(rna_path).has_value();
}
static bool is_idproperty_keyable(const IDProperty *id_prop, PointerRNA *ptr, PropertyRNA *prop)
{
/* While you can cast the IDProperty* to a PropertyRNA* and pass it to the RNA_* functions, this
* does not work because it will not have the right flags set. Instead the resolved
* PointerRNA and PropertyRNA need to be passed. */
if (!RNA_property_anim_editable(ptr, prop)) {
return false;
}
if (ELEM(id_prop->type,
eIDPropertyType::IDP_BOOLEAN,
eIDPropertyType::IDP_INT,
eIDPropertyType::IDP_FLOAT,
eIDPropertyType::IDP_DOUBLE))
{
return true;
}
if (id_prop->type == eIDPropertyType::IDP_ARRAY) {
if (ELEM(id_prop->subtype,
eIDPropertyType::IDP_BOOLEAN,
eIDPropertyType::IDP_INT,
eIDPropertyType::IDP_FLOAT,
eIDPropertyType::IDP_DOUBLE))
{
return true;
}
}
return false;
}
Vector<RNAPath> get_keyable_id_property_paths(const PointerRNA &ptr)
{
IDProperty *properties;
if (ptr.type == RNA_PoseBone) {
const bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr.data);
properties = pchan->prop;
}
else if (ptr.type == RNA_Object) {
const Object *ob = static_cast<Object *>(ptr.data);
properties = ob->id.properties;
}
else {
/* Pointer type not supported. */
return {};
}
if (!properties) {
return {};
}
Vector<RNAPath> paths;
for (const IDProperty &id_prop : properties->data.group) {
PointerRNA resolved_ptr;
PropertyRNA *resolved_prop;
std::string path = id_prop.name;
/* Resolving the path twice, once as RNA property (without brackets, `"propname"`),
* and once as ID property (with brackets, `["propname"]`).
* This is required to support IDProperties that have been defined as part of an add-on.
* Those need to be animated through an RNA path without the brackets. */
bool is_resolved = RNA_path_resolve_property(
&ptr, path.c_str(), &resolved_ptr, &resolved_prop);
/* ID properties can be named the same as internal properties, for example `scale`. In that
* case they would resolve, but it wouldn't be the correct property. `RNA_property_is_runtime`
* catches that case. */
if (!is_resolved || !RNA_property_is_runtime(resolved_prop)) {
char name_escaped[MAX_IDPROP_NAME * 2];
BLI_str_escape(name_escaped, id_prop.name, sizeof(name_escaped));
path = fmt::format("[\"{}\"]", name_escaped);
is_resolved = RNA_path_resolve_property(&ptr, path.c_str(), &resolved_ptr, &resolved_prop);
}
if (!is_resolved) {
continue;
}
if (is_idproperty_keyable(&id_prop, &resolved_ptr, resolved_prop)) {
paths.append({path});
}
}
return paths;
}
Array<float> rna_property_get_as_float(PointerRNA &ptr, PropertyRNA &prop)
{
const bool is_array = RNA_property_array_check(&prop);
Array<float> values;
if (is_array) {
values.reinitialize(RNA_property_array_length(&ptr, &prop));
}
else {
values.reinitialize(1);
}
switch (RNA_property_type(&prop)) {
case PROP_BOOLEAN:
if (is_array) {
for (const int i : values.index_range()) {
values[i] = RNA_property_boolean_get_index(&ptr, &prop, i);
}
}
else {
values[0] = RNA_property_boolean_get(&ptr, &prop);
}
break;
case PROP_INT:
if (is_array) {
for (const int i : values.index_range()) {
values[i] = RNA_property_int_get_index(&ptr, &prop, i);
}
}
else {
values[0] = RNA_property_int_get(&ptr, &prop);
}
break;
case PROP_FLOAT:
if (is_array) {
RNA_property_float_get_array(&ptr, &prop, values.data());
}
else {
values[0] = RNA_property_float_get(&ptr, &prop);
}
break;
default:
/* Unsupported property type. */
return {};
}
return values;
}
void rna_property_set_as_float(PointerRNA &ptr, PropertyRNA &prop, const Span<float> values)
{
const bool is_array = RNA_property_array_check(&prop);
if (is_array && RNA_property_array_length(&ptr, &prop) != values.size()) {
/* Array length has to match. */
BLI_assert_unreachable();
return;
}
switch (RNA_property_type(&prop)) {
case PROP_BOOLEAN:
if (is_array) {
for (const int i : values.index_range()) {
RNA_property_boolean_set_index(&ptr, &prop, i, values[i]);
}
}
else {
RNA_property_boolean_set(&ptr, &prop, values[0]);
}
break;
case PROP_INT:
if (is_array) {
for (const int i : values.index_range()) {
RNA_property_int_set_index(&ptr, &prop, i, values[i]);
}
}
else {
RNA_property_int_set(&ptr, &prop, values[0]);
}
break;
case PROP_FLOAT:
if (is_array) {
RNA_property_float_set_array(&ptr, &prop, values.data());
}
else {
RNA_property_float_set(&ptr, &prop, values[0]);
}
break;
default:
/* Unsupported property type. */
BLI_assert_unreachable();
return;
}
}
} // namespace blender::animrig

View File

@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_rna.hh"
#include "BKE_gtest_base.hh"
#include "testing/testing.h"
namespace blender::animrig::tests {
class AnimRnaTest : public bke::BlenderGTestBase {};
TEST_F(AnimRnaTest, is_rotation_path)
{
EXPECT_TRUE(is_rotation_path("rotation_euler"));
EXPECT_TRUE(is_rotation_path("pose.bones[\"test\"].rotation_euler"));
EXPECT_FALSE(is_rotation_path("xrotation_euler"));
EXPECT_FALSE(is_rotation_path("rotation_euler2"));
EXPECT_FALSE(is_rotation_path("[\"rotation_euler\"]"));
EXPECT_FALSE(is_rotation_path("pose.bones[\"test\"][\"rotation_euler\"]"));
}
TEST_F(AnimRnaTest, rotation_mode_from_path)
{
EXPECT_EQ(ROT_MODE_QUAT, get_rotation_mode_from_path("rotation_quaternion").value());
EXPECT_EQ(ROT_MODE_EUL, get_rotation_mode_from_path("rotation_euler").value());
EXPECT_EQ(ROT_MODE_EUL,
get_rotation_mode_from_path("pose.bones[\"test\"].rotation_euler").value());
EXPECT_EQ(ROT_MODE_AXISANGLE, get_rotation_mode_from_path("rotation_axis_angle").value());
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("scale"));
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("xrotation_euler"));
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("rotation_euler2"));
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,366 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_action.hh"
#include "ANIM_animdata.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_fcurve.hh"
#include "BKE_key.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_material.hh"
#include "BKE_node.hh"
#include "BLT_translation.hh"
#include "BLI_listbase.h"
#include "BLI_string_utf8.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DNA_anim_types.h"
#include "DNA_key_types.h"
#include "DNA_material_types.h"
#include "DNA_particle_types.h"
#include "RNA_access.hh"
namespace blender::animrig {
/* -------------------------------------------------------------------- */
/** \name Public F-Curves API
* \{ */
/* Find the users of the given ID within the objects of `bmain` and add non-duplicates to the end
* of `related_ids`. */
static void add_object_data_users(const Main &bmain, const ID &id, Vector<ID *> &related_ids)
{
if (ID_REAL_USERS(&id) != 1) {
/* Only find objects if this ID is only used once. */
return;
}
Object *ob;
ID *object_id;
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
ob = reinterpret_cast<Object *>(object_id);
if (ob->data != &id) {
continue;
}
related_ids.append_non_duplicates(&ob->id);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
Vector<ID *> find_related_ids(Main &bmain, ID &id)
{
Vector<ID *> related_ids({&id});
/* `related_ids` can grow during an iteration if the ID of the current iteration has associated
* code that defines relationships. */
for (int i = 0; i < related_ids.size(); i++) {
ID *related_id = related_ids[i];
if (related_id->flag & ID_FLAG_EMBEDDED_DATA) {
/* No matter the type of embedded ID, their owner can always be added to the related IDs. */
/* User counting is irrelevant for the logic here, because embedded IDs cannot be shared.
* Embedded IDs do exist (sometimes) with a non-zero user count, hence the assertion that the
* user count is not greater than 1. */
BLI_assert(ID_REAL_USERS(related_id) <= 1);
ID *owner_id = BKE_id_owner_get(related_id);
/* Embedded IDs should always have an owner. */
BLI_assert(owner_id != nullptr);
related_ids.append_non_duplicates(owner_id);
}
/* No action found on current ID, add related IDs to the ID Vector. */
switch (GS(related_id->name)) {
case ID_OB: {
Object *ob = reinterpret_cast<Object *>(related_id);
if (!ob->data) {
break;
}
ID *data = ob->data;
if (ID_REAL_USERS(data) == 1) {
related_ids.append_non_duplicates(data);
}
for (ParticleSystem &particle_system : ob->particlesystem) {
if (!particle_system.part) {
continue;
}
if (ID_REAL_USERS(&particle_system.part->id) != 1) {
continue;
}
related_ids.append_non_duplicates(&particle_system.part->id);
}
break;
}
case ID_KE: {
/* Shape-keys. */
Key *key = reinterpret_cast<Key *>(related_id);
/* Shape-keys are not embedded but there is currently no way to reuse them. */
BLI_assert(ID_REAL_USERS(related_id) == 1);
related_ids.append_non_duplicates(key->from);
break;
}
case ID_MA: {
/* Explicitly not relating materials and material users. */
Material *mat = reinterpret_cast<Material *>(related_id);
if (mat->nodetree && ID_REAL_USERS(&mat->nodetree->id) == 1) {
related_ids.append_non_duplicates(&mat->nodetree->id);
}
break;
}
case ID_PA: {
if (ID_REAL_USERS(related_id) != 1) {
continue;
}
Object *ob;
ID *object_id;
/* Find users of this particle setting. */
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
ob = reinterpret_cast<Object *>(object_id);
bool object_uses_particle_settings = false;
for (ParticleSystem &particle_system : ob->particlesystem) {
if (!particle_system.part) {
continue;
}
if (&particle_system.part->id != related_id) {
continue;
}
object_uses_particle_settings = true;
break;
}
if (object_uses_particle_settings) {
related_ids.append_non_duplicates(&ob->id);
break;
}
}
FOREACH_MAIN_LISTBASE_ID_END;
break;
}
default: {
/* Just check if the ID is used as object data somewhere. */
add_object_data_users(bmain, *related_id, related_ids);
bNodeTree *node_tree = bke::node_tree_from_id(related_id);
if (node_tree && ID_REAL_USERS(&node_tree->id) == 1) {
related_ids.append_non_duplicates(&node_tree->id);
}
Key *key = BKE_key_from_id(related_id);
if (key) {
/* No check for multi user because the shape-key cannot be shared. */
BLI_assert(ID_REAL_USERS(&key->id) == 1);
related_ids.append_non_duplicates(&key->id);
}
break;
}
}
}
return related_ids;
}
/* Find an action on an ID that is related to the given ID. Related things are e.g. Object<->Data,
* Mesh<->Material and so on. */
static bAction *find_related_action(Main &bmain, ID &id)
{
Vector<ID *> related_ids = find_related_ids(bmain, id);
for (ID *related_id : related_ids) {
Action *action = get_action(*related_id);
if (action && BKE_id_is_editable(&bmain, &action->id)) {
/* Returning the first action found means highest priority has the action closest in the
* relationship graph. */
return action;
}
}
return nullptr;
}
bAction *id_action_ensure(Main *bmain, ID *id)
{
AnimData *adt = BKE_animdata_ensure_id(id);
if (adt == nullptr) {
printf("ERROR: data-block type is not animatable (ID = %s)\n", (id) ? (id->name) : "<None>");
return nullptr;
}
/* init action if none available yet */
/* TODO: need some wizardry to handle NLA stuff correct */
if (adt->action == nullptr) {
bAction *action = find_related_action(*bmain, *id);
if (action == nullptr) {
/* init action name from name of ID block */
char actname[sizeof(id->name) - 2];
if (id->flag & ID_FLAG_EMBEDDED_DATA) {
/* When the ID is embedded, use the name of the owner ID for clarity. */
ID *owner_id = BKE_id_owner_get(id);
/* If the ID is embedded it should have an owner. */
BLI_assert(owner_id != nullptr);
SNPRINTF_UTF8(actname, DATA_("%sAction"), owner_id->name + 2);
}
else if (GS(id->name) == ID_KE) {
Key *key = reinterpret_cast<Key *>(id);
SNPRINTF_UTF8(actname, DATA_("%sAction"), key->from->name + 2);
}
else {
SNPRINTF_UTF8(actname, DATA_("%sAction"), id->name + 2);
}
/* create action */
action = BKE_action_add(bmain, actname);
/* Decrement the default-1 user count, as assigning it will increase it again. */
BLI_assert(action->id.us == 1);
id_us_min(&action->id);
}
/* Assigning the Action should always work here. The only reason it wouldn't, is when a legacy
* Action of the wrong ID type is assigned, but since in this branch of the code we're only
* dealing with either new or layered Actions, this will never fail. */
const bool ok = animrig::assign_action(action, {*id, *adt});
BLI_assert_msg(ok, "Expecting Action assignment to work here");
UNUSED_VARS_NDEBUG(ok);
/* Tag depsgraph to be rebuilt to include time dependency. */
DEG_relations_tag_update(bmain);
}
DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
/* return the action */
return adt->action;
}
void animdata_fcurve_delete(AnimData *adt, FCurve *fcu)
{
/* - If no AnimData, we've got nowhere to remove the F-Curve from
* (this doesn't guarantee that the F-Curve is in there, but at least we tried).
* - If no F-Curve, there is nothing to remove
*/
if (ELEM(nullptr, adt, fcu)) {
return;
}
const bool is_driver = fcu->driver != nullptr;
if (is_driver) {
BLI_remlink(&adt->drivers, fcu);
}
else if (adt->action) {
Action &action = adt->action->wrap();
action_fcurve_remove(action, *fcu);
/* Return early to avoid the call to BKE_fcurve_free because the fcu has already been freed
* by action_fcurve_remove. */
return;
}
else {
BLI_assert_unreachable();
}
BKE_fcurve_free(fcu);
}
bool animdata_remove_empty_action(AnimData *adt)
{
if (adt->action != nullptr) {
bAction *act = adt->action;
DEG_id_tag_update(&act->id, ID_RECALC_ANIMATION_NO_FLUSH);
Action &action = act->wrap();
if (action.is_empty() && (adt->flag & ADT_NLA_EDIT_ON) == 0) {
id_us_min(&act->id);
adt->action = nullptr;
return true;
}
}
return false;
}
/** \} */
const FCurve *fcurve_find_by_rna_path(const AnimData &adt,
const StringRefNull rna_path,
const int array_index)
{
BLI_assert(adt.action);
if (!adt.action) {
return nullptr;
}
const Action &action = adt.action->wrap();
const Slot *slot = action.slot_for_handle(adt.slot_handle);
if (!slot) {
/* No need to inspect anything if this ID does not have an Action Slot. */
return nullptr;
}
/* No check for the slot's ID type. Not only do we not have the actual ID
* to do this check, but also, since the Action and the slot have been
* assigned, just trust that it's valid. */
/* Iterate the layers top-down, as higher-up animation overrides (or at least can override)
* lower-down animation. */
for (int layer_idx = action.layer_array_num - 1; layer_idx >= 0; layer_idx--) {
const Layer *layer = action.layer(layer_idx);
/* TODO: refactor this into something nicer once we have different strip types. */
for (const Strip *strip : layer->strips()) {
switch (strip->type()) {
case Strip::Type::Keyframe: {
const StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);
const Channelbag *channelbag_for_slot = strip_data.channelbag_for_slot(*slot);
if (!channelbag_for_slot) {
continue;
}
const FCurve *fcu = channelbag_for_slot->fcurve_find({rna_path, array_index});
if (!fcu) {
continue;
}
/* This code assumes that there is only one strip, and that it's infinite. When that
* changes, this code needs to be expanded to check for strip boundaries. */
return fcu;
}
}
/* Explicit lack of 'default' clause, to get compiler warnings when strip types are added. */
}
}
return nullptr;
}
Span<FCurve *> fcurves_for_assigned_action(AnimData *adt)
{
if (!adt || !adt->action) {
return {};
}
return fcurves_for_action_slot(adt->action->wrap(), adt->slot_handle);
}
Span<const FCurve *> fcurves_for_assigned_action(const AnimData *adt)
{
if (!adt || !adt->action) {
return {};
}
return fcurves_for_action_slot(const_cast<const bAction *>(adt->action)->wrap(),
adt->slot_handle);
}
} // namespace blender::animrig

View File

@@ -0,0 +1,76 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_armature.hh"
#include "BKE_action.hh"
#include "BKE_pose.hh"
#include "BLI_listbase.h"
#include "DNA_object_types.h"
namespace blender::animrig {
void pose_bone_descendent_iterator(Object &pose_ob,
bPoseChannel &pchan,
FunctionRef<void(bPoseChannel &child_bone)> callback)
{
/* Needed for fast name lookups. */
BKE_pose_channels_hash_ensure(pose_ob.pose);
int i = 0;
/* This is not using an std::deque because the implementation of that has issues on windows. */
Vector<bPoseChannel *> descendants = {&pchan};
while (i < descendants.size()) {
bPoseChannel *descendant = descendants[i];
i++;
callback(*descendant);
Bone *descendant_bone = descendant->bone_get(pose_ob);
for (Bone &child_bone : descendant_bone->childbase) {
bPoseChannel *child_pose_bone = BKE_pose_channel_find_name(pose_ob.pose, child_bone.name);
if (!child_pose_bone) {
/* Can happen if the pose is not rebuilt. */
BLI_assert_unreachable();
continue;
}
descendants.append(child_pose_bone);
}
}
};
static bool pose_depth_iterator_recursive(Object &pose_ob,
bke::PChanBone pchanbone,
FunctionRef<bool(bPoseChannel &child_bone)> callback)
{
if (!callback(*pchanbone.pchan)) {
return false;
}
bool success = true;
for (Bone &child_bone : pchanbone.bone->childbase) {
bPoseChannel *child_pose_bone = BKE_pose_channel_find_name(pose_ob.pose, child_bone.name);
if (!child_pose_bone) {
BLI_assert_unreachable();
success = false;
continue;
}
success &= pose_depth_iterator_recursive(pose_ob, {child_pose_bone, &child_bone}, callback);
}
return success;
}
bool pose_bone_descendent_depth_iterator(Object &pose_ob,
bPoseChannel &pchan,
FunctionRef<bool(bPoseChannel &child_bone)> callback)
{
/* Needed for fast name lookups. */
BKE_pose_channels_hash_ensure(pose_ob.pose);
return pose_depth_iterator_recursive(pose_ob, {&pchan, pchan.bone_get(pose_ob)}, callback);
}
} // namespace blender::animrig

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Internal C++ functions to deal with bone collections. These are mostly here for internal
* use in `bone_collections.cc` and have them testable by unit tests.
*/
#pragma once
namespace blender {
struct bArmature;
struct BoneCollection;
namespace animrig::internal {
/**
* Move a block of BoneCollections in the Armature's `collections_array`, from
* `start_index` to `start_index + direction`.
*
* The move operation is actually implemented as a rotation, so that no
* `BoneCollection*` is lost. In other words, one of these operations is
* performed, depending on `direction`. Here `B` indicates an element in the
* moved block, and `X` indicates the rotated element.
*
* direction = +1: [. . . X B B B B . . .] -> [. . . B B B B X . . .]
* direction = -1: [. . . B B B B X . . .] -> [. . . X B B B B . . .]
*
* This function does not alter the length of `collections_array`.
* It only performs the rotation, and updates any `child_index` when they
* reference elements of the moved block.
*
* It also does not touch any `child_count` properties of bone collections.
* Updating those, as well as any references to the rotated element, is the
* responsibility of the caller.
*
* \param direction: Must be either -1 or 1.
*/
void bonecolls_rotate_block(bArmature *armature, int start_index, int count, int direction);
/**
* Move a bone collection to another index.
*
* This is implemented via a call to #bonecolls_rotate_block, so all the
* documentation of that function (including its invariants and caveats) applies
* here too.
*/
void bonecolls_move_to_index(bArmature *armature, int from_index, int to_index);
/**
* Find the given bone collection in the armature's collections, and return its index.
*
* The bone collection is only searched for at the given index, index+1, and index-1.
*
* If the bone collection cannot be found, -1 is returned.
*/
int bonecolls_find_index_near(bArmature *armature, BoneCollection *bcoll, int index);
void bonecolls_debug_list(const bArmature *armature);
/**
* Unassign all (edit)bones from this bone collection, and free it.
*
* Note that this does NOT take care of updating the collection hierarchy information. See
* #ANIM_armature_bonecoll_remove_from_index and #ANIM_armature_bonecoll_remove for that.
*/
void bonecoll_unassign_and_free(bArmature *armature, BoneCollection *bcoll);
} // namespace animrig::internal
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_bonecolor.hh"
#include "BLI_hash.hh"
#include "DNA_action_types.h"
#include "UI_resources.hh"
#include <cstring>
namespace blender::animrig {
BoneColor::BoneColor()
{
this->palette_index = 0;
}
BoneColor::BoneColor(const BoneColor &other)
{
this->palette_index = other.palette_index;
std::memcpy(&this->custom, &other.custom, sizeof(this->custom));
}
BoneColor::~BoneColor() = default;
const ThemeWireColor *BoneColor::effective_color() const
{
const int8_t color_index = this->palette_index;
if (color_index == 0) {
return nullptr;
}
if (color_index < 0) {
return &this->custom;
}
const bTheme *btheme = ui::theme::theme_get();
return &btheme->tarm[(color_index - 1)];
}
bool BoneColor::operator==(const BoneColor &other) const
{
if (palette_index != other.palette_index) {
return false;
}
if (palette_index == -1) {
/* Explicitly compare each field, skipping the DNA padding fields. */
/* TODO: maybe there is already a DNA-level-comparison function for this? */
/* The last byte of the colors isn't used, but it's still in memory. The annoying thing is that
* values are inconsistently either 0 or 255 depending on how the color was set, and there is
* no way to influence this with the color picker in the GUI. So, just skip the last byte in
* the comparisons. */
return std::memcmp(custom.solid, other.custom.solid, sizeof(custom.solid) - 1) == 0 &&
std::memcmp(custom.select, other.custom.select, sizeof(custom.select) - 1) == 0 &&
std::memcmp(custom.active, other.custom.active, sizeof(custom.active) - 1) == 0 &&
custom.flag == other.custom.flag;
}
return true;
}
bool BoneColor::operator!=(const BoneColor &other) const
{
return !(*this == other);
}
uint64_t BoneColor::hash() const
{
if (palette_index >= 0) {
/* Theme colors are simple. */
return get_default_hash(palette_index);
}
/* For custom colors, hash everything together. */
/* The last byte of the color is skipped, as it is inconsistent (see note above). */
const uint64_t hash_solid = get_default_hash(custom.solid[0], custom.solid[1], custom.solid[2]);
const uint64_t hash_select = get_default_hash(
custom.select[0], custom.select[1], custom.select[2]);
const uint64_t hash_active = get_default_hash(
custom.active[0], custom.active[1], custom.active[2]);
return get_default_hash(hash_solid, hash_select, hash_active, custom.flag);
}
const BoneColor &ANIM_bonecolor_posebone_get(const bke::PChanBoneConst pchanbone)
{
if (pchanbone.pchan->color.palette_index == 0) {
return pchanbone.bone->color.wrap();
}
return pchanbone.pchan->color.wrap();
}
}; // namespace blender::animrig

View File

@@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_driver.hh"
#include "BKE_fcurve_driver.h"
#include "DNA_anim_types.h"
#include "RNA_access.hh"
namespace blender::animrig {
float evaluate_driver_from_rna_pointer(const AnimationEvalContext *anim_eval_context,
PointerRNA *ptr,
PropertyRNA *prop,
const FCurve *fcu)
{
PathResolvedRNA anim_rna;
if (!RNA_path_resolved_create(ptr, prop, fcu->array_index, &anim_rna)) {
return 0.0f;
}
return evaluate_driver(&anim_rna, fcu->driver, fcu->driver, anim_eval_context);
}
} // namespace blender::animrig

View File

@@ -0,0 +1,331 @@
/* SPDX-FileCopyrightText: 2023 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_evaluation.hh"
#include "BKE_animsys.h"
#include "BKE_fcurve.hh"
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_task.hh"
#include "CLG_log.h"
#include "evaluation_internal.hh"
namespace blender {
static CLG_LogRef LOG = {"anim.evaluation"};
namespace animrig {
using namespace internal;
/**
* Blend the intermediate_result into the final_result based on the layer
* weight and mix mode.
*/
void blend_layer_results(EvaluationResult &final_result,
const EvaluationResult &intermediate_result,
const Layer &current_layer);
/**
* Apply the result of the animation evaluation to the given data-block.
*
* \param flush_to_original: when true, look up the original data-block (assuming the given one is
* an evaluated copy) and update that too.
*/
void apply_evaluation_result(const EvaluationResult &evaluation_result,
PointerRNA &animated_id_ptr,
bool flush_to_original);
EvaluationResult evaluate_action(PointerRNA &animated_id_ptr,
Action &action,
const slot_handle_t slot_handle,
const AnimationEvalContext &anim_eval_context)
{
EvaluationResult result;
/* Evaluate each layer in order. */
for (Layer *layer : action.layers()) {
if (layer->influence <= 0.0f) {
/* Don't bother evaluating layers without influence. */
continue;
}
EvaluationResult layer_result = evaluate_layer(
animated_id_ptr, action, *layer, slot_handle, anim_eval_context);
if (!layer_result) {
continue;
}
if (!result) {
/* Simple case: no results so far, so just use this layer as-is. There is
* nothing to blend/combine with, so ignore the influence and combination
* options. */
result = std::move(layer_result);
continue;
}
/* Complex case: blend this layer's result into combined result. */
blend_layer_results(result, layer_result, *layer);
}
return result;
}
void evaluate_and_apply_action(PointerRNA &animated_id_ptr,
Action &action,
const slot_handle_t slot_handle,
const AnimationEvalContext &anim_eval_context,
const bool flush_to_original)
{
EvaluationResult evaluation_result = evaluate_action(
animated_id_ptr, action, slot_handle, anim_eval_context);
if (!evaluation_result) {
return;
}
apply_evaluation_result(evaluation_result, animated_id_ptr, flush_to_original);
}
/* Copy of the same-named function in anim_sys.cc, with the check on action groups removed. */
static bool is_fcurve_evaluatable(const FCurve *fcu)
{
if (fcu->rna_path == nullptr) {
return false;
}
/* Not checking for FCURVE_DISABLED here, because those FCurves may still be evaluatable for
* other users of the same slot. See #135666. This is safe to do since this function isn't called
* for drivers. */
if (fcu->flag & FCURVE_MUTED) {
return false;
}
if (BKE_fcurve_is_empty(fcu)) {
return false;
}
return true;
}
/* Copy of the same-named function in anim_sys.cc, but with the special handling for NLA strips
* removed. */
static void animsys_construct_orig_pointer_rna(const PointerRNA *ptr, PointerRNA *ptr_orig)
{
*ptr_orig = *ptr;
/* Original note from anim_sys.cc:
* -----------
* NOTE: nlastrip_evaluate_controls() creates PointerRNA with ID of nullptr. Technically, this is
* not a valid pointer, but there are exceptions in various places of this file which handles
* such pointers.
* We do special trickery here as well, to quickly go from evaluated to original NlaStrip.
* -----------
* And this is all not ported to the new layered animation system. */
BLI_assert_msg(ptr->owner_id, "NLA support was not ported to the layered animation system");
ptr_orig->owner_id = ptr_orig->owner_id->orig_id;
ptr_orig->data = ptr_orig->owner_id;
}
/* Copy of the same-named function in anim_sys.cc. */
static void animsys_write_orig_anim_rna(PointerRNA *ptr,
const char *rna_path,
const int array_index,
const float value)
{
PointerRNA ptr_orig;
animsys_construct_orig_pointer_rna(ptr, &ptr_orig);
PathResolvedRNA orig_anim_rna;
/* TODO(sergey): Should be possible to cache resolved path in dependency graph somehow. */
if (BKE_animsys_rna_path_resolve(&ptr_orig, rna_path, array_index, &orig_anim_rna)) {
BKE_animsys_write_to_rna_path(&orig_anim_rna, value);
}
}
static EvaluationResult evaluate_keyframe_data(PointerRNA &animated_id_ptr,
StripKeyframeData &strip_data,
const slot_handle_t slot_handle,
const AnimationEvalContext &offset_eval_context)
{
Channelbag *channelbag_for_slot = strip_data.channelbag_for_slot(slot_handle);
if (!channelbag_for_slot) {
return {};
}
Span<FCurve *> fcurves = channelbag_for_slot->fcurves();
/* Stores true for FCurves that have been evaluated. Not using BitVector because writing to it
* from threads will introduce race conditions.*/
Array<bool> valid(fcurves.size(), false);
Array<float> results(fcurves.size());
Array<PathResolvedRNA> resolved_rna(fcurves.size());
threading::parallel_for(fcurves.index_range(), 512, [&](const IndexRange range) {
for (const int i : range) {
FCurve *fcu = fcurves[i];
if (!is_fcurve_evaluatable(fcu)) {
continue;
}
/* Resolve the RNA path to skip unresolvable properties. It's faster to do that in a thread
* and store the result for later. */
PathResolvedRNA &anim_rna = resolved_rna[i];
if (!BKE_animsys_rna_path_resolve(
&animated_id_ptr, fcu->rna_path, fcu->array_index, &anim_rna))
{
continue;
}
BLI_assert(fcu->driver == nullptr);
/* Not using calculate_fcurve because FCurves of channelbags are not drivers. */
results[i] = evaluate_fcurve(fcu, offset_eval_context.eval_time);
valid[i] = true;
}
});
EvaluationResult evaluation_result;
evaluation_result.reserve(fcurves.size());
for (const int i : fcurves.index_range()) {
if (!valid[i]) {
continue;
}
FCurve *fcu = fcurves[i];
PathResolvedRNA &anim_rna = resolved_rna[i];
/* This part is not threadsafe. */
evaluation_result.store(fcu->rna_path, fcu->array_index, results[i], anim_rna);
}
return evaluation_result;
}
void apply_evaluation_result(const EvaluationResult &evaluation_result,
PointerRNA &animated_id_ptr,
const bool flush_to_original)
{
for (const auto &channel_result : evaluation_result.items()) {
const PropIdentifier &prop_ident = channel_result.key;
const AnimatedProperty &anim_prop = channel_result.value;
const float animated_value = anim_prop.value;
PathResolvedRNA anim_rna = anim_prop.prop_rna;
BKE_animsys_write_to_rna_path(&anim_rna, animated_value);
if (flush_to_original) {
/* Convert the StringRef to a `const char *`, as the rest of the RNA path handling code in
* BKE still uses `char *` instead of `StringRef`. */
animsys_write_orig_anim_rna(
&animated_id_ptr, prop_ident.rna_path.c_str(), prop_ident.array_index, animated_value);
}
}
}
static EvaluationResult evaluate_strip(PointerRNA &animated_id_ptr,
Action &owning_action,
Strip &strip,
const slot_handle_t slot_handle,
const AnimationEvalContext &anim_eval_context)
{
AnimationEvalContext offset_eval_context = anim_eval_context;
/* Positive offset means the entire strip is pushed "to the right", so
* evaluation needs to happen further "to the left". */
offset_eval_context.eval_time -= strip.frame_offset;
switch (strip.type()) {
case Strip::Type::Keyframe: {
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(owning_action);
return evaluate_keyframe_data(animated_id_ptr, strip_data, slot_handle, offset_eval_context);
}
}
return {};
}
void blend_layer_results(EvaluationResult &final_result,
const EvaluationResult &intermediate_result,
const Layer &current_layer)
{
/* TODO?: store the layer results sequentially, so that we can step through
* them in parallel, instead of iterating over one and doing map lookups on
* the other. */
for (const auto &channel_result : intermediate_result.items()) {
const PropIdentifier &prop_ident = channel_result.key;
AnimatedProperty *last_prop = final_result.lookup_ptr(prop_ident);
const AnimatedProperty &anim_prop = channel_result.value;
if (!last_prop) {
/* Nothing to blend with, so just take (influence * value). */
final_result.store(prop_ident.rna_path,
prop_ident.array_index,
anim_prop.value * current_layer.influence,
anim_prop.prop_rna);
continue;
}
/* TODO: move this to a separate function. And write more smartness for rotations. */
switch (current_layer.mix_mode()) {
case Layer::MixMode::Replace:
last_prop->value = anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Offset:
last_prop->value = math::interpolate(
current_layer.influence, last_prop->value, anim_prop.value);
break;
case Layer::MixMode::Add:
last_prop->value += anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Subtract:
last_prop->value -= anim_prop.value * current_layer.influence;
break;
case Layer::MixMode::Multiply:
last_prop->value *= anim_prop.value * current_layer.influence;
break;
};
}
}
namespace internal {
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
Action &owning_action,
Layer &layer,
const slot_handle_t slot_handle,
const AnimationEvalContext &anim_eval_context)
{
/* TODO: implement cross-blending between overlapping strips. For now, this is not supported.
* Instead, the first strong result is taken (see below), and if that is not available, the last
* weak result will be used.
*
* Weak result: obtained from evaluating the final frame of the strip.
* Strong result: any result that is not a weak result. */
EvaluationResult last_weak_result;
for (Strip *strip : layer.strips()) {
if (!strip->contains_frame(anim_eval_context.eval_time)) {
continue;
}
/* Cannot use const here because the std::move would not work otherwise. */
EvaluationResult strip_result = evaluate_strip(
animated_id_ptr, owning_action, *strip, slot_handle, anim_eval_context);
if (!strip_result) {
continue;
}
const bool is_weak_result = strip->is_last_frame(anim_eval_context.eval_time);
if (is_weak_result) {
/* Keep going until a strong result is found. */
last_weak_result = std::move(strip_result);
continue;
}
/* Found a strong result, just return it. */
return strip_result;
}
return last_weak_result;
}
} // namespace internal
} // namespace animrig
} // namespace blender

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2024 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "ANIM_evaluation.hh"
namespace blender {
struct Action;
struct Layer;
struct AnimationEvalContext;
struct PointerRNA;
namespace animrig::internal {
/**
* Evaluate the animation data on the given layer, for the given slot. This
* just returns the evaluation result, without taking any other layers,
* blending, influence, etc. into account.
*/
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
Action &owning_action,
Layer &layer,
slot_handle_t slot_handle,
const AnimationEvalContext &anim_eval_context);
} // namespace animrig::internal
} // namespace blender

View File

@@ -0,0 +1,310 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_evaluation.hh"
#include "evaluation_internal.hh"
#include "BKE_action.hh"
#include "BKE_animsys.h"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "BLI_math_base.h"
#include <optional>
#include "testing/testing.h"
namespace blender::animrig::tests {
using namespace blender::animrig::internal;
class AnimationEvaluationTest : public bke::BlenderGTestBase {
protected:
Main *bmain;
Action *action;
Object *cube;
Slot *slot;
Layer *layer;
KeyframeSettings settings = get_keyframe_settings(false);
AnimationEvalContext anim_eval_context = {};
PointerRNA cube_rna_ptr;
public:
void SetUp() override
{
bmain = BKE_main_new();
action = BKE_id_new<Action>(bmain, "ACÄnimåtië");
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
slot = &action->slot_add();
ASSERT_EQ(assign_action_and_slot(action, slot, cube->id), ActionSlotAssignmentResult::OK);
layer = &action->layer_add("Kübus layer");
/* Make it easier to predict test values. */
settings.interpolation = BEZT_IPO_LIN;
cube_rna_ptr = RNA_pointer_create_discrete(&cube->id, RNA_Object, &cube->id);
}
void TearDown() override
{
BKE_main_free(bmain);
}
/** Evaluate the layer, and return result for the given property. */
std::optional<float> evaluate_single_property(const StringRefNull rna_path,
const int array_index,
const float eval_time)
{
anim_eval_context.eval_time = eval_time;
EvaluationResult result = evaluate_layer(
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
const AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier(rna_path, array_index));
if (!loc0_result) {
return {};
}
return loc0_result->value;
}
/** Evaluate the layer, and test that the given property evaluates to the expected value. */
testing::AssertionResult test_evaluate_layer(const StringRefNull rna_path,
const int array_index,
const float2 eval_time__expect_value)
{
const float eval_time = eval_time__expect_value[0];
const float expect_value = eval_time__expect_value[1];
const std::optional<float> opt_eval_value = evaluate_single_property(
rna_path, array_index, eval_time);
if (!opt_eval_value) {
return testing::AssertionFailure()
<< rna_path << "[" << array_index << "] should have been animated";
}
const float eval_value = *opt_eval_value;
const uint diff_ulps = ulp_diff_ff(expect_value, eval_value);
if (diff_ulps >= 4) {
return testing::AssertionFailure()
<< std::endl
<< " " << rna_path << "[" << array_index
<< "] evaluation did not produce the expected result:" << std::endl
<< " evaluated to: " << testing::PrintToString(eval_value) << std::endl
<< " expected : " << testing::PrintToString(expect_value) << std::endl;
}
return testing::AssertionSuccess();
};
/** Evaluate the layer, and test that the given property is not part of the result. */
testing::AssertionResult test_evaluate_layer_no_result(const StringRefNull rna_path,
const int array_index,
const float eval_time)
{
const std::optional<float> eval_value = evaluate_single_property(
rna_path, array_index, eval_time);
if (eval_value) {
return testing::AssertionFailure()
<< std::endl
<< " " << rna_path << "[" << array_index
<< "] evaluation should NOT produce a value:" << std::endl
<< " evaluated to: " << testing::PrintToString(*eval_value) << std::endl;
}
return testing::AssertionSuccess();
}
};
TEST_F(AnimationEvaluationTest, evaluate_layer__keyframes)
{
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
/* Set some keys. */
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.1f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 47.5f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {1.0f, 0.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {5.0f, 3.14f}, settings);
/* Set the animated properties to some values. These should not be overwritten
* by the evaluation itself. */
cube->loc[0] = 3.0f;
cube->loc[1] = 2.0f;
cube->loc[2] = 7.0f;
cube->rot[0] = 3.0f;
cube->rot[1] = 2.0f;
cube->rot[2] = 7.0f;
/* Evaluate. */
anim_eval_context.eval_time = 3.0f;
EvaluationResult result = evaluate_layer(
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
/* Check the result. */
ASSERT_FALSE(result.is_empty());
AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier("location", 0));
ASSERT_NE(nullptr, loc0_result) << "location[0] should have been animated";
EXPECT_EQ(47.3f, loc0_result->value);
EXPECT_EQ(3.0f, cube->loc[0]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(2.0f, cube->loc[1]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(7.0f, cube->loc[2]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(3.0f, cube->rot[0]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(2.0f, cube->rot[1]) << "Evaluation should not modify the animated ID";
EXPECT_EQ(7.0f, cube->rot[2]) << "Evaluation should not modify the animated ID";
}
TEST_F(AnimationEvaluationTest, strip_boundaries__single_strip)
{
/* Single finite strip, check first, middle, and last frame. */
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
strip.resize(1.0f, 10.0f);
/* Set some keys. */
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
/* Evaluate the layer to see how it handles the boundaries + something in between. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
}
TEST_F(AnimationEvaluationTest, strip_boundaries__nonoverlapping)
{
/* Two finite strips that are strictly distinct. */
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(11.0f, 20.0f);
strip2.frame_offset = 10;
/* Set some keys. */
{
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
{
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
/* Check Strip 1. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
/* Check Strip 2. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {11.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {13.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {20.0f, 48.0f}));
/* Check outside the range of the strips. */
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 20.001f));
}
TEST_F(AnimationEvaluationTest, strip_boundaries__overlapping_edge)
{
/* Two finite strips that are overlapping on their edge. */
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
strip1.resize(1.0f, 10.0f);
strip2.resize(10.0f, 19.0f);
strip2.frame_offset = 9;
/* Set some keys. */
{
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
{
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
}
/* Check Strip 1. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
/* Check overlapping frame. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 47.0f}))
<< "On the overlapping frame, only Strip 2 should be evaluated.";
/* Check Strip 2. */
EXPECT_TRUE(test_evaluate_layer("location", 0, {12.0f, 187.0f}));
EXPECT_TRUE(test_evaluate_layer("location", 0, {19.0f, 48.0f}));
/* Check outside the range of the strips. */
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 19.001f));
}
class AccessibleEvaluationResult : public EvaluationResult {
public:
EvaluationMap &get_map()
{
return result_;
}
};
class AnimationEvaluationResultTest : public bke::BlenderGTestBase {};
TEST_F(AnimationEvaluationResultTest, prop_identifier_hashing)
{
AccessibleEvaluationResult result;
/* Test storing the same result twice, with different memory locations of the RNA paths. This
* tests that the mapping uses the actual string, and not just pointer comparison. */
const char *rna_path_1 = "pose.bones['Root'].location";
const std::string rna_path_2(rna_path_1);
ASSERT_NE(rna_path_1, rna_path_2.c_str())
<< "This test requires different addresses for the RNA path strings";
PathResolvedRNA fake_resolved_rna;
result.store(rna_path_1, 0, 1.0f, fake_resolved_rna);
result.store(rna_path_2, 0, 2.0f, fake_resolved_rna);
EXPECT_EQ(1, result.get_map().size())
<< "Storing a result for the same property twice should just overwrite the previous value";
{
PropIdentifier key(rna_path_1, 0);
AnimatedProperty *anim_prop = result.lookup_ptr(key);
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
}
{
PropIdentifier key(rna_path_2, 0);
AnimatedProperty *anim_prop = result.lookup_ptr(key);
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
}
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,717 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include <cfloat>
#include <cmath>
#include <cstring>
#include "ANIM_animdata.hh"
#include "ANIM_fcurve.hh"
#include "BKE_fcurve.hh"
#include "BLI_math_base.h"
#include "BLI_math_vector_types.hh"
#include "BLI_string.h"
#include "DNA_anim_types.h"
#include "MEM_guardedalloc.h"
namespace blender::animrig {
KeyframeSettings get_keyframe_settings(const bool from_userprefs)
{
KeyframeSettings settings = {};
settings.keyframe_type = BEZT_KEYTYPE_KEYFRAME;
settings.handle = HD_AUTO_ANIM;
settings.interpolation = BEZT_IPO_BEZ;
if (from_userprefs) {
settings.interpolation = eBezTriple_Interpolation(U.ipo_new);
settings.handle = eBezTriple_Handle(U.keyhandles_new);
}
return settings;
}
const FCurve *fcurve_find(Span<const FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor)
{
for (const FCurve *fcurve : fcurves) {
/* Check indices first, much cheaper than a string comparison. */
if (fcurve->array_index == fcurve_descriptor.array_index && fcurve->rna_path &&
StringRef(fcurve->rna_path) == fcurve_descriptor.rna_path)
{
return fcurve;
}
}
return nullptr;
}
FCurve *fcurve_find(Span<FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor)
{
const FCurve *fcurve = fcurve_find(fcurves.cast<const FCurve *>(), fcurve_descriptor);
return const_cast<FCurve *>(fcurve);
}
FCurve *create_fcurve_for_channel(const FCurveDescriptor &fcurve_descriptor)
{
FCurve *fcu = BKE_fcurve_create();
fcu->rna_path = BLI_strdupn(fcurve_descriptor.rna_path.data(),
fcurve_descriptor.rna_path.size());
fcu->array_index = fcurve_descriptor.array_index;
fcu->flag = (FCURVE_VISIBLE | FCURVE_SELECTED);
fcu->auto_smoothing = U.auto_smoothing_new;
if (fcurve_descriptor.prop_type.has_value()) {
fcu->flag |= fcurve_flags_for_property_type(*fcurve_descriptor.prop_type);
}
/* Set the fcurve's color mode if needed/able. */
if ((U.keying_flag & KEYING_FLAG_XYZ2RGB) != 0 && fcurve_descriptor.prop_subtype.has_value()) {
switch (*fcurve_descriptor.prop_subtype) {
case PROP_TRANSLATION:
case PROP_XYZ:
case PROP_EULER:
case PROP_COLOR:
case PROP_COORDS:
fcu->color_mode = FCURVE_COLOR_AUTO_RGB;
break;
case PROP_QUATERNION:
fcu->color_mode = FCURVE_COLOR_AUTO_YRGB;
break;
default:
/* Leave the color mode as default. */
break;
}
}
return fcu;
}
eFCurve_Flags fcurve_flags_for_property_type(const PropertyType prop_type)
{
switch (prop_type) {
case PROP_FLOAT:
return eFCurve_Flags{};
case PROP_INT:
/* Do integer (only 'whole' numbers) interpolation between all points. */
return FCURVE_INT_VALUES;
default:
/* Do 'discrete' (i.e. enum, boolean values which cannot take any intermediate
* values at all) interpolation between all points.
* - however, we must also ensure that evaluated values are only integers still.
*/
return FCURVE_DISCRETE_VALUES | FCURVE_INT_VALUES;
}
}
bool fcurve_delete_keyframe_at_time(FCurve *fcurve, const float time)
{
if (!fcurve || BKE_fcurve_is_protected(*fcurve)) {
return false;
}
bool found;
const int index = BKE_fcurve_bezt_binarysearch_index(
fcurve->bezt, time, fcurve->totvert, &found);
if (!found) {
return false;
}
BKE_fcurve_delete_key(fcurve, index);
BKE_fcurve_handles_recalc(*fcurve);
return true;
}
bool delete_keyframe_fcurve_legacy(AnimData *adt, FCurve *fcu, float cfra)
{
if (!fcurve_delete_keyframe_at_time(fcu, cfra)) {
return false;
}
/* Empty curves get automatically deleted. */
if (BKE_fcurve_is_empty(fcu)) {
animdata_fcurve_delete(adt, fcu);
}
return true;
}
/* ************************************************** */
/* KEYFRAME INSERTION */
/* -------------- BezTriple Insertion -------------------- */
/* Change the Y position of a keyframe to match the input, adjusting handles. */
static void replace_bezt_keyframe_ypos(BezTriple *dst, const BezTriple *bezt)
{
/* Just change the values when replacing, so as to not overwrite handles. */
float dy = bezt->vec[1][1] - dst->vec[1][1];
/* Just apply delta value change to the handle values. */
dst->vec[0][1] += dy;
dst->vec[1][1] += dy;
dst->vec[2][1] += dy;
dst->f1 = bezt->f1;
dst->f2 = bezt->f2;
dst->f3 = bezt->f3;
/* TODO: perform some other operations? */
}
int insert_bezt_fcurve(FCurve *fcu, const BezTriple *bezt, eInsertKeyFlags flag)
{
int i = 0;
/* Are there already keyframes? */
if (fcu->bezt) {
bool replace;
i = BKE_fcurve_bezt_binarysearch_index(fcu->bezt, bezt->vec[1][0], fcu->totvert, &replace);
/* Replace an existing keyframe? */
if (replace) {
/* `i` may in rare cases exceed array bounds. */
if ((i >= 0) && (i < fcu->totvert)) {
if (flag & INSERTKEY_OVERWRITE_FULL) {
fcu->bezt[i] = *bezt;
}
else {
replace_bezt_keyframe_ypos(&fcu->bezt[i], bezt);
}
if (flag & INSERTKEY_CYCLE_AWARE) {
/* If replacing an end point of a cyclic curve without offset,
* modify the other end too. */
if (ELEM(i, 0, fcu->totvert - 1) && BKE_fcurve_get_cycle_type(*fcu) == FCU_CYCLE_PERFECT)
{
replace_bezt_keyframe_ypos(&fcu->bezt[i == 0 ? fcu->totvert - 1 : 0], bezt);
}
}
}
}
/* Keyframing modes allow not replacing the keyframe. */
else if ((flag & INSERTKEY_REPLACE) == 0) {
/* Insert new - if we're not restricted to replacing keyframes only. */
BezTriple *newb = MEM_new_array_zeroed<BezTriple>(fcu->totvert + 1, "beztriple");
/* Add the beztriples that should occur before the beztriple to be pasted
* (originally in fcu). */
if (i > 0) {
memcpy(newb, fcu->bezt, i * sizeof(BezTriple));
}
/* Add beztriple to paste at index i. */
*(newb + i) = *bezt;
/* Add the beztriples that occur after the beztriple to be pasted (originally in fcu). */
if (i < fcu->totvert) {
memcpy(newb + i + 1, fcu->bezt + i, (fcu->totvert - i) * sizeof(BezTriple));
}
/* Replace (+ free) old with new, only if necessary to do so. */
MEM_delete(fcu->bezt);
fcu->bezt = newb;
fcu->totvert++;
}
else {
return -1;
}
}
/* No keyframes yet, but can only add if...
* 1) keyframing modes say that keyframes can only be replaced, so adding new ones won't know
* 2) there are no samples on the curve
* NOTE: maybe we may want to allow this later when doing samples -> bezt conversions,
* but for now, having both is asking for trouble
*/
else if ((flag & INSERTKEY_REPLACE) == 0 && (fcu->fpt == nullptr)) {
/* Create new keyframes array. */
fcu->bezt = MEM_new_zeroed<BezTriple>("beztriple");
*(fcu->bezt) = *bezt;
fcu->totvert = 1;
}
/* Cannot add anything. */
else {
/* Return error code -1 to prevent any misunderstandings. */
return -1;
}
/* We need to return the index, so that some tools which do post-processing can
* detect where we added the BezTriple in the array.
*/
return i;
}
/**
* Update the FCurve to allow insertion of `bezt` without modifying the curve shape.
*
* Checks whether it is necessary to apply Bezier subdivision due to involvement of non-auto
* handles. If necessary, changes `bezt` handles from Auto to Aligned.
*
* \param bezt: key being inserted
* \param prev: keyframe before that key
* \param next: keyframe after that key
*/
static void subdivide_nonauto_handles(const FCurve *fcu,
BezTriple *bezt,
BezTriple *prev,
BezTriple *next)
{
if (prev->ipo != BEZT_IPO_BEZ || bezt->ipo != BEZT_IPO_BEZ) {
return;
}
/* Don't change Vector handles, or completely auto regions. */
const bool bezt_auto = BEZT_IS_AUTOH(bezt) || (bezt->h1 == HD_VECT && bezt->h2 == HD_VECT);
const bool prev_auto = BEZT_IS_AUTOH(prev) || (prev->h2 == HD_VECT);
const bool next_auto = BEZT_IS_AUTOH(next) || (next->h1 == HD_VECT);
if (bezt_auto && prev_auto && next_auto) {
return;
}
/* Subdivide the curve. */
float delta;
if (!BKE_fcurve_bezt_subdivide_handles(bezt, prev, next, &delta)) {
return;
}
/* Decide when to force auto to manual. */
if (!BEZT_IS_AUTOH(bezt)) {
return;
}
if ((prev_auto || next_auto) && fcu->auto_smoothing == FCURVE_SMOOTH_CONT_ACCEL) {
const float hx = bezt->vec[1][0] - bezt->vec[0][0];
const float dx = bezt->vec[1][0] - prev->vec[1][0];
/* This mode always uses 1/3 of key distance for handle x size. */
const bool auto_works_well = fabsf(hx - dx / 3.0f) < 0.001f;
if (auto_works_well) {
return;
}
}
/* Turn off auto mode. */
bezt->h1 = bezt->h2 = HD_ALIGN;
}
void initialize_bezt(BezTriple *beztr,
const float2 position,
const KeyframeSettings &settings,
const eFCurve_Flags fcu_flags)
{
/* Set all three points, for nicer start position.
* NOTE: +/- 1 on vec.x for left and right handles is so that 'free' handles work ok...
*/
beztr->vec[0][0] = position.x - 1.0f;
beztr->vec[0][1] = position.y;
beztr->vec[1][0] = position.x;
beztr->vec[1][1] = position.y;
beztr->vec[2][0] = position.x + 1.0f;
beztr->vec[2][1] = position.y;
beztr->f1 = beztr->f2 = beztr->f3 = BEZT_FLAG_SELECT;
beztr->h1 = beztr->h2 = settings.handle;
beztr->ipo = settings.interpolation;
/* Interpolation type used is constrained by the type of values the curve can take. */
if (fcu_flags & FCURVE_DISCRETE_VALUES) {
beztr->ipo = BEZT_IPO_CONST;
}
else if ((beztr->ipo == BEZT_IPO_BEZ) && (fcu_flags & FCURVE_INT_VALUES)) {
beztr->ipo = BEZT_IPO_LIN;
}
/* Set keyframe type value (supplied),
* which should come from the scene settings in most cases. */
BEZKEYTYPE_LVALUE(beztr) = settings.keyframe_type;
/* Set default values for "easing" interpolation mode settings.
* NOTE: Even if these modes aren't currently used, if users switch
* to these later, we want these to work in a sane way out of
* the box.
*/
/* "back" easing - This value used to be used when overshoot=0, but that
* introduced discontinuities in how the param worked. */
beztr->back = 1.70158f;
/* "elastic" easing - Values here were hand-optimized for a default duration of
* ~10 frames (typical motion-graph motion length). */
beztr->amplitude = 0.8f;
beztr->period = 4.1f;
}
/**
* Return whether the given fcurve already evaluates to the same value as the
* proposed keyframe at the keyframe's time.
*
* This is a helper function for determining whether to insert a keyframe or not
* when "only insert needed" is enabled.
*
* NOTE: this does *not* determine whether inserting the keyframe would change
* the fcurve at points other than the keyframe itself. For example, even if
* inserting the key wouldn't change the fcurve's value at the time of the
* keyframe, the resulting changes to bezier interpolation could change the
* fcurve on either side of it. This function intentionally does not account for
* that, since that's not how the "only insert needed" feature is supposed to
* work.
*/
static bool new_key_needed(const FCurve &fcu, const float frame, const float value)
{
if (fcu.totvert == 0) {
return true;
}
bool replace;
const int bezt_index = BKE_fcurve_bezt_binarysearch_index(
fcu.bezt, frame, fcu.totvert, &replace);
if (replace) {
/* If there is already a key, we only need to modify it if the proposed value is different. */
return fcu.bezt[bezt_index].vec[1][1] != value;
}
const int diff_ulp = 32;
const float fcu_eval = evaluate_fcurve(&fcu, frame);
/* No need to insert a key if the same value is already the value of the FCurve at that point. */
if (compare_ff_relative(fcu_eval, value, FLT_EPSILON, diff_ulp)) {
return false;
}
return true;
}
/**
* Move the point where a key is about to be inserted to be inside the main cycle range.
* Returns the type of the cycle if it is enabled and valid.
*/
static float2 remap_cyclic_keyframe_location(const FCurve &fcu,
const eFCU_Cycle_Type type,
float2 position)
{
if (fcu.totvert < 2 || !fcu.bezt) {
return position;
}
if (type == FCU_CYCLE_NONE) {
return position;
}
BezTriple *first = &fcu.bezt[0], *last = &fcu.bezt[fcu.totvert - 1];
const float start = first->vec[1][0], end = last->vec[1][0];
if (start >= end) {
return position;
}
if (position.x < start || position.x > end) {
const float period = end - start;
const float step = floorf((position.x - start) / period);
position.x -= step * period;
if (type == FCU_CYCLE_OFFSET) {
/* Nasty check to handle the case when the modes are different better. */
FMod_Cycles *data = static_cast<FMod_Cycles *>(
static_cast<FModifier *>(fcu.modifiers.first)->data);
short mode = (step >= 0) ? data->after_mode : data->before_mode;
if (mode == FCM_EXTRAPOLATE_CYCLIC_OFFSET) {
position.y -= step * (last->vec[1][1] - first->vec[1][1]);
}
}
}
return position;
}
SingleKeyingResult insert_vert_fcurve(FCurve *fcu,
const float2 position,
const KeyframeSettings &settings,
eInsertKeyFlags flag)
{
BLI_assert(fcu != nullptr);
float2 remapped_position = position;
/* Adjust coordinates for cycle aware insertion. */
if (flag & INSERTKEY_CYCLE_AWARE) {
eFCU_Cycle_Type type = BKE_fcurve_get_cycle_type(*fcu);
remapped_position = remap_cyclic_keyframe_location(*fcu, type, position);
if (type != FCU_CYCLE_PERFECT) {
/* Inhibit action from insert_bezt_fcurve unless it's a perfect cycle. */
flag &= ~INSERTKEY_CYCLE_AWARE;
}
}
if ((flag & INSERTKEY_NEEDED) && !new_key_needed(*fcu, remapped_position.x, remapped_position.y))
{
return SingleKeyingResult::NO_KEY_NEEDED;
}
BezTriple beztr = {{{0}}};
initialize_bezt(&beztr, remapped_position, settings, eFCurve_Flags(fcu->flag));
uint oldTot = fcu->totvert;
int a;
/* Add temp beztriple to keyframes. */
a = insert_bezt_fcurve(fcu, &beztr, flag);
BKE_fcurve_active_keyframe_set(fcu, &fcu->bezt[a]);
/* Key insertion failed. */
if (a < 0) {
/* TODO: we need more info from `insert_bezt_fcurve()` called above to
* return a more specific failure. */
return SingleKeyingResult::UNKNOWN_FAILURE;
}
/* Set handle-type and interpolation. */
if ((fcu->totvert > 2) && (flag & INSERTKEY_REPLACE) == 0) {
BezTriple *bezt = (fcu->bezt + a);
/* Set interpolation from previous (if available),
* but only if we didn't just replace some keyframe:
* - Replacement is indicated by no-change in number of verts.
* - When replacing, the user may have specified some interpolation that should be kept.
*/
if (fcu->totvert > oldTot) {
if (a > 0) {
bezt->ipo = (bezt - 1)->ipo;
}
else if (a < fcu->totvert - 1) {
bezt->ipo = (bezt + 1)->ipo;
}
if (0 < a && a < (fcu->totvert - 1) && (flag & INSERTKEY_OVERWRITE_FULL) == 0) {
subdivide_nonauto_handles(fcu, bezt, bezt - 1, bezt + 1);
}
}
}
/* Don't recalculate handles if fast is set.
* - this is a hack to make importers faster
* - we may calculate twice (due to auto-handle needing to be calculated twice)
*/
if ((flag & INSERTKEY_FAST) == 0) {
BKE_fcurve_handles_recalc(*fcu);
}
/* Return the index at which the keyframe was added. */
return SingleKeyingResult::SUCCESS;
}
void sample_fcurve_segment(const FCurve *fcu,
const float start_frame,
const float sample_rate,
float *samples,
const int sample_count)
{
for (int i = 0; i < sample_count; i++) {
const float evaluation_time = start_frame + (float(i) / sample_rate);
samples[i] = evaluate_fcurve(fcu, evaluation_time);
}
}
static void remove_fcurve_key_range(FCurve *fcu,
const int2 range,
const BakeCurveRemove removal_mode)
{
switch (removal_mode) {
case BakeCurveRemove::ALL: {
BKE_fcurve_delete_keys_all(*fcu);
break;
}
case BakeCurveRemove::OUT_RANGE: {
bool replace;
int before_index = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, range[0], fcu->totvert, &replace);
if (before_index > 0) {
BKE_fcurve_delete_keys(*fcu, {0, uint(before_index)});
}
int after_index = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, range[1], fcu->totvert, &replace);
/* #OUT_RANGE is treated as exclusive on both ends. */
if (replace) {
after_index++;
}
if (after_index < fcu->totvert) {
BKE_fcurve_delete_keys(*fcu, {uint(after_index), fcu->totvert});
}
break;
}
case BakeCurveRemove::IN_RANGE: {
bool replace;
const int range_start_index = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, range[0], fcu->totvert, &replace);
int range_end_index = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, range[1], fcu->totvert, &replace);
if (replace) {
range_end_index++;
}
if (range_end_index > range_start_index) {
BKE_fcurve_delete_keys(*fcu, {uint(range_start_index), uint(range_end_index)});
}
break;
}
default:
break;
}
}
void bake_fcurve(FCurve *fcu,
const int2 range,
const float step,
const BakeCurveRemove remove_existing)
{
BLI_assert(step > 0);
const int sample_count = (range[1] - range[0]) / step + 1;
float *samples = MEM_new_array_zeroed<float>(sample_count, "Channel Bake Samples");
const float sample_rate = 1.0f / step;
sample_fcurve_segment(fcu, range[0], sample_rate, samples, sample_count);
if (remove_existing != BakeCurveRemove::NONE) {
remove_fcurve_key_range(fcu, range, remove_existing);
}
BezTriple *baked_keys = MEM_new_array_zeroed<BezTriple>(sample_count, "beztriple");
const KeyframeSettings settings = get_keyframe_settings(true);
for (int i = 0; i < sample_count; i++) {
BezTriple *key = &baked_keys[i];
float2 key_position = {range[0] + i * step, samples[i]};
initialize_bezt(key, key_position, settings, eFCurve_Flags(fcu->flag));
}
int merged_size;
BezTriple *merged_bezt = BKE_bezier_array_merge(
baked_keys, sample_count, fcu->bezt, fcu->totvert, &merged_size);
if (fcu->bezt != nullptr) {
/* Can happen if we removed all keys beforehand. */
MEM_delete(fcu->bezt);
}
MEM_delete(baked_keys);
fcu->bezt = merged_bezt;
fcu->totvert = merged_size;
MEM_delete(samples);
BKE_fcurve_handles_recalc(*fcu);
}
struct TempFrameValCache {
float frame, val;
};
void bake_fcurve_segments(FCurve *fcu)
{
const BezTriple *bezt, *start = nullptr, *end = nullptr;
TempFrameValCache *value_cache, *fp;
int sfra, range;
int i, n;
if (fcu->bezt == nullptr) {
return;
}
KeyframeSettings settings = get_keyframe_settings(true);
settings.keyframe_type = BEZT_KEYTYPE_BREAKDOWN;
/* Find selected keyframes... once pair has been found, add keyframes. */
for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
/* check if selected, and which end this is */
if (BEZT_ISSEL_ANY(bezt)) {
if (start) {
/* If next bezt is also selected, don't start sampling yet,
* but instead wait for that one to reconsider, to avoid
* changing the curve when sampling consecutive segments
* (#53229)
*/
if (i < fcu->totvert - 1) {
BezTriple *next = &fcu->bezt[i + 1];
if (BEZT_ISSEL_ANY(next)) {
continue;
}
}
end = bezt;
/* Cache values then add keyframes using these values, as adding
* keyframes while sampling will affect the outcome...
* - Only start sampling+adding from index=1, so that we don't overwrite original keyframe.
*/
range = int(ceil(end->vec[1][0] - start->vec[1][0]));
sfra = int(floor(start->vec[1][0]));
if (range) {
value_cache = MEM_new_array_zeroed<TempFrameValCache>(range, "IcuFrameValCache");
/* Sample values. */
for (n = 1, fp = value_cache; n < range && fp; n++, fp++) {
fp->frame = float(sfra + n);
fp->val = evaluate_fcurve(fcu, fp->frame);
}
/* Add keyframes with these, tagging as 'breakdowns'. */
for (n = 1, fp = value_cache; n < range && fp; n++, fp++) {
animrig::insert_vert_fcurve(fcu, {fp->frame, fp->val}, settings, INSERTKEY_NOFLAGS);
}
MEM_delete(value_cache);
/* As we added keyframes, we need to compensate so that bezt is at the right place. */
bezt = fcu->bezt + i + range - 1;
i += (range - 1);
}
/* The current selection island has ended, so start again from scratch. */
start = nullptr;
end = nullptr;
}
else {
/* Just set start keyframe. */
start = bezt;
end = nullptr;
}
}
}
BKE_fcurve_handles_recalc(*fcu);
}
bool fcurve_frame_has_keyframe(const FCurve *fcu, const float frame)
{
if (ELEM(nullptr, fcu, fcu->bezt)) {
return false;
}
if ((fcu->flag & FCURVE_MUTED) == 0) {
bool replace;
const int i = BKE_fcurve_bezt_binarysearch_index(fcu->bezt, frame, fcu->totvert, &replace);
/* #BKE_fcurve_bezt_binarysearch_index will set replace to be 0 or 1
* - obviously, 1 represents a match
*/
if (replace) {
/* `i` may in rare cases exceed array bounds. */
if ((i >= 0) && (i < fcu->totvert)) {
return true;
}
}
}
return false;
}
} // namespace blender::animrig

View File

@@ -0,0 +1,915 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include <cmath>
#include <string>
#include <fmt/format.h>
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_animdata.hh"
#include "ANIM_fcurve.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_rna.hh"
#include "ANIM_visualkey.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_fcurve.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_nla.hh"
#include "BKE_report.hh"
#include "DNA_scene_types.h"
#include "BLI_math_base.h"
#include "BLI_task.hh"
#include "BLI_utildefines.h"
#include "BLT_translation.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_anim_types.h"
#include "MEM_guardedalloc.h"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"
#include "WM_types.hh"
namespace blender::animrig {
void generate_single_keying_result_report(const SingleKeyingResult result, ReportList *reports)
{
switch (result) {
case SingleKeyingResult::SUCCESS:
BKE_reportf(reports, RPT_INFO, "Successfully inserted a key.");
break;
case SingleKeyingResult::UNKNOWN_FAILURE:
BKE_reportf(reports, RPT_ERROR, "Keyframe insertion failed for an unknown reason.");
break;
case SingleKeyingResult::CANNOT_CREATE_FCURVE:
BKE_reportf(reports, RPT_ERROR, "Failed to create the F-Curve.");
break;
case SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE:
BKE_reportf(reports, RPT_ERROR, "The F-Curve is not keyable. It may be locked or sampled.");
break;
case SingleKeyingResult::NO_KEY_NEEDED:
BKE_reportf(
reports, RPT_ERROR, "Due to the setting 'Only Insert Needed' no keyframe was inserted.");
break;
case SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK:
BKE_reportf(reports, RPT_ERROR, "Due to the NLA stack setup, no key was inserted.");
break;
case SingleKeyingResult::ID_NOT_EDITABLE:
BKE_reportf(
reports, RPT_ERROR, "Inserting key has been skipped because the ID cannot be edited.");
break;
case SingleKeyingResult::ID_NOT_ANIMATABLE:
BKE_reportf(
reports, RPT_ERROR, "Inserting key has been skipped because the ID cannot be keyed.");
break;
case SingleKeyingResult::NO_VALID_LAYER:
BKE_reportf(reports, RPT_ERROR, "No valid layer. Cannot insert key.");
break;
case SingleKeyingResult::NO_VALID_STRIP:
BKE_reportf(reports, RPT_ERROR, "No valid strip. Cannot insert key.");
break;
case SingleKeyingResult::NO_VALID_SLOT:
BKE_reportf(reports, RPT_ERROR, "No valid slot. Cannot insert key.");
break;
case SingleKeyingResult::CANNOT_RESOLVE_PATH:
BKE_reportf(reports, RPT_ERROR, "Invalid RNA path. Cannot insert key.");
break;
case SingleKeyingResult::_KEYING_RESULT_MAX:
break;
}
}
CombinedKeyingResult::CombinedKeyingResult()
{
result_counter.fill(0);
}
void CombinedKeyingResult::add(const SingleKeyingResult result, const int count)
{
result_counter[int(result)] += count;
}
void CombinedKeyingResult::merge(const CombinedKeyingResult &other)
{
for (int i = 0; i < result_counter.size(); i++) {
result_counter[i] += other.result_counter[i];
}
}
int CombinedKeyingResult::get_count(const SingleKeyingResult result) const
{
return result_counter[int(result)];
}
bool CombinedKeyingResult::has_errors() const
{
/* For loop starts at 1 to skip the SUCCESS flag. Assumes that SUCCESS is 0 and the rest of the
* enum are sequential values. */
static_assert(int(SingleKeyingResult::SUCCESS) == 0);
for (int i = 1; i < result_counter.size(); i++) {
if (result_counter[i] > 0) {
return true;
}
}
return false;
}
void CombinedKeyingResult::generate_reports(ReportList *reports, const eReportType report_level)
{
if (!this->has_errors() && this->get_count(SingleKeyingResult::SUCCESS) == 0) {
BKE_reportf(
reports, RPT_WARNING, "No keys have been inserted and no errors have been reported.");
return;
}
Vector<std::string> errors;
if (this->get_count(SingleKeyingResult::UNKNOWN_FAILURE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::UNKNOWN_FAILURE);
errors.append(fmt::format(
fmt::runtime(RPT_("There were {:d} keying failures for unknown reasons.")), error_count));
}
if (this->get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE);
errors.append(fmt::format(
fmt::runtime(RPT_("Could not create {:d} F-Curve(s). This can happen when only "
"inserting to available F-Curves.")),
error_count));
}
if (this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE);
errors.append(
fmt::format(fmt::runtime(RPT_(
"{:d} F-Curve(s) are not keyframeable. They might be locked or sampled.")),
error_count));
}
if (this->get_count(SingleKeyingResult::NO_KEY_NEEDED) > 0) {
const int error_count = this->get_count(SingleKeyingResult::NO_KEY_NEEDED);
errors.append(fmt::format(
fmt::runtime(RPT_(
"Due to the setting 'Only Insert Needed', {:d} keyframe(s) have not been inserted.")),
error_count));
}
if (this->get_count(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK) > 0) {
const int error_count = this->get_count(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK);
errors.append(fmt::format(
fmt::runtime(RPT_("Due to the NLA stack setup, {:d} keyframe(s) have not been inserted.")),
error_count));
}
if (this->get_count(SingleKeyingResult::ID_NOT_EDITABLE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::ID_NOT_EDITABLE);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"they are not editable.")),
error_count));
}
if (this->get_count(SingleKeyingResult::ID_NOT_ANIMATABLE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::ID_NOT_ANIMATABLE);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"they cannot be animated.")),
error_count));
}
if (this->get_count(SingleKeyingResult::CANNOT_RESOLVE_PATH) > 0) {
const int error_count = this->get_count(SingleKeyingResult::CANNOT_RESOLVE_PATH);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"the RNA path wasn't valid for them.")),
error_count));
}
if (this->get_count(SingleKeyingResult::NO_VALID_LAYER) > 0) {
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_LAYER);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"there were no layers that could accept the keys.")),
error_count));
}
if (this->get_count(SingleKeyingResult::NO_VALID_STRIP) > 0) {
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_STRIP);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"there were no strips that could accept the keys.")),
error_count));
}
if (this->get_count(SingleKeyingResult::NO_VALID_SLOT) > 0) {
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_SLOT);
errors.append(fmt::format(
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
"of missing action slots.")),
error_count));
}
if (errors.is_empty()) {
BKE_report(reports, RPT_WARNING, "Encountered unhandled error during keyframing");
return;
}
if (errors.size() == 1) {
BKE_report(reports, report_level, errors[0].c_str());
return;
}
std::string error_message = RPT_("Inserting keyframes failed:");
for (const std::string &error : errors) {
error_message.append(fmt::format("\n- {}", error));
}
BKE_report(reports, report_level, error_message.c_str());
}
std::optional<StringRefNull> default_channel_group_for_path(const PointerRNA *animated_struct,
const StringRef prop_rna_path)
{
if (animated_struct->type == RNA_PoseBone) {
bPoseChannel *pose_channel = static_cast<bPoseChannel *>(animated_struct->data);
return pose_channel->name;
}
if (animated_struct->type == RNA_Object) {
if (prop_rna_path.find("location") != StringRef::not_found ||
prop_rna_path.find("rotation") != StringRef::not_found ||
prop_rna_path.find("scale") != StringRef::not_found)
{
/* NOTE: Keep this label in sync with the "ID" case in
* _keyingsets_utils.py :: get_transform_generators_base_info()
*/
return "Object Transforms";
}
}
return std::nullopt;
}
void update_autoflags_fcurve_direct(FCurve *fcu, const PropertyType prop_type)
{
/* First clear out all the flags that should be updated by this function, before setting just the
* ones suitable for this property type. */
fcu->flag &= ~(FCURVE_INT_VALUES | FCURVE_DISCRETE_VALUES);
fcu->flag |= fcurve_flags_for_property_type(prop_type);
}
bool is_keying_flag(const Scene *scene, const eKeying_Flag flag)
{
if (scene) {
return (scene->toolsettings->keying_flag & flag) || (U.keying_flag & flag);
}
return U.keying_flag & flag;
}
eInsertKeyFlags get_keyframing_flags(Scene *scene)
{
eInsertKeyFlags flag = INSERTKEY_NOFLAGS;
/* Visual keying. */
if (is_keying_flag(scene, KEYING_FLAG_VISUALKEY)) {
flag |= INSERTKEY_MATRIX;
}
/* Cycle-aware keyframe insertion - preserve cycle period and flow. */
if (is_keying_flag(scene, KEYING_FLAG_CYCLEAWARE)) {
flag |= INSERTKEY_CYCLE_AWARE;
}
if (is_keying_flag(scene, MANUALKEY_FLAG_INSERTNEEDED)) {
flag |= INSERTKEY_NEEDED;
}
return flag;
}
/**
* Checks whether the Action assigned to `adt` (if any) has any keyframes at the
* given frame. Since we're only concerned whether a keyframe exists, we can
* simply loop until a match is found.
*
* For layered actions, this only checks for keyframes in the assigned slot.
*/
static bool assigned_action_has_keyframe_at(AnimData &adt, const float frame)
{
if (adt.action == nullptr) {
return false;
}
if (adt.action->flag & ACT_MUTED) {
return false;
}
const Span<FCurve *> fcurves = animrig::fcurves_for_assigned_action(&adt);
/* 1024 is a common value for memory bandwidth limited tasks. The number isn't critical: 512
* works fine here, but 128 and 4096 seem to work equally well in testing. */
return threading::parallel_reduce<bool>(
fcurves.index_range(),
512,
false,
[&](const IndexRange range, const bool is_found) {
if (is_found) {
return true;
}
for (FCurve *fcu : fcurves.slice(range)) {
if (fcurve_frame_has_keyframe(fcu, frame)) {
return true;
}
}
return false;
},
std::logical_or<bool>());
}
/* Checks whether an Object has a keyframe for a given frame. */
static bool object_frame_has_keyframe(Object *ob, const float frame)
{
if (ob == nullptr) {
return false;
}
/* Check its own animation data - specifically, the action it contains. */
if ((ob->adt) && (ob->adt->action)) {
/* #41525 - When the active action is a NLA strip being edited,
* we need to correct the frame number to "look inside" the
* remapped action
*/
const float ob_frame = BKE_nla_tweakedit_remap(ob->adt, frame, NLATIME_CONVERT_UNMAP);
if (assigned_action_has_keyframe_at(*ob->adt, ob_frame)) {
return true;
}
}
/* nothing found */
return false;
}
bool id_frame_has_keyframe(ID *id, float frame)
{
if (id == nullptr) {
return false;
}
/* Perform special checks for 'macro' types. */
switch (GS(id->name)) {
case ID_OB:
return object_frame_has_keyframe(id_cast<Object *>(id), frame);
default: {
AnimData *adt = BKE_animdata_from_id(id);
/* only check keyframes in active action */
if (adt) {
return assigned_action_has_keyframe_at(*adt, frame);
}
break;
}
}
return false;
}
bool key_insertion_may_create_fcurve(const eInsertKeyFlags insert_key_flags)
{
return (insert_key_flags & (INSERTKEY_REPLACE | INSERTKEY_AVAILABLE)) == 0;
}
Vector<float> get_property_values(PointerRNA *ptr, PropertyRNA *prop, const bool visual_key)
{
Vector<float> values;
if (visual_key && visualkey_can_use(ptr, prop)) {
/* Visual-keying is only available for object data-blocks and pose-channels,
* as it works by key-framing using a value extracted from the final matrix
* instead of using the kt system to extract a value. */
values = visualkey_get_values(ptr, prop);
}
else {
values = get_rna_values(ptr, prop);
}
return values;
}
static float nla_time_remap(float time,
const AnimationEvalContext *anim_eval_context,
PointerRNA *id_ptr,
AnimData *adt,
bAction *act,
ListBaseT<NlaKeyframingContext> *nla_cache,
NlaKeyframingContext **r_nla_context)
{
if (adt && adt->action == act) {
*r_nla_context = BKE_animsys_get_nla_keyframing_context(
nla_cache, id_ptr, adt, anim_eval_context);
const float remapped_frame = BKE_nla_tweakedit_remap(adt, time, NLATIME_CONVERT_UNMAP);
return remapped_frame;
}
*r_nla_context = nullptr;
return time;
}
SingleKeyingResult insert_keyframe_direct(PointerRNA &ptr,
PropertyRNA &prop,
FCurve &fcu,
const float fcurve_frame,
const eBezTriple_KeyframeType keytype,
const eInsertKeyFlags flag)
{
if ((ptr.owner_id == nullptr) && (ptr.data == nullptr)) {
BLI_assert_unreachable();
return SingleKeyingResult::UNKNOWN_FAILURE;
}
if (!BKE_fcurve_is_keyframable(fcu)) {
return SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE;
}
/* Update F-Curve flags to ensure proper behavior for property type. */
update_autoflags_fcurve_direct(&fcu, RNA_property_type(&prop));
const bool visual_keyframing = flag & INSERTKEY_MATRIX;
Vector<float> values = get_property_values(&ptr, &prop, visual_keyframing);
const int index = fcu.array_index;
if (index < 0 || index >= values.size()) {
/* Can only happen if the FCurve and PropertyRNA do not match which
* should never be the case. */
BLI_assert_unreachable();
return SingleKeyingResult::UNKNOWN_FAILURE;
}
KeyframeSettings settings = get_keyframe_settings((flag & INSERTKEY_NO_USERPREF) == 0);
settings.keyframe_type = keytype;
return insert_vert_fcurve(&fcu, {fcurve_frame, values[index]}, settings, flag);
}
/* ************************************************** */
/* KEYFRAME DELETION */
/* Main Keyframing API call:
* Use this when validation of necessary animation data isn't necessary as it
* already exists. It will delete a keyframe at the current frame.
*
* The flag argument is used for special settings that alter the behavior of
* the keyframe deletion. These include the quick refresh options.
*/
static void deg_tag_after_keyframe_delete(Main *bmain, ID *id, AnimData *adt)
{
if (adt->action == nullptr) {
/* In the case last f-curve was removed need to inform dependency graph
* about relations update, since it needs to get rid of animation operation
* for this data-block. */
DEG_id_tag_update_ex(bmain, id, ID_RECALC_ANIMATION_NO_FLUSH);
DEG_relations_tag_update(bmain);
}
else {
DEG_id_tag_update_ex(bmain, &adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
}
}
int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path, float cfra)
{
AnimData *adt = BKE_animdata_from_id(id);
if (ELEM(nullptr, id, adt)) {
BKE_report(reports, RPT_ERROR, "No ID block and/or AnimData to delete keyframe from");
return 0;
}
PointerRNA ptr;
PropertyRNA *prop;
PointerRNA id_ptr = RNA_id_pointer_create(id);
if (RNA_path_resolve_property(&id_ptr, rna_path.path.c_str(), &ptr, &prop) == false) {
BKE_reportf(
reports,
RPT_ERROR,
"Could not delete keyframe, as RNA path is invalid for the given ID (ID = %s, path = %s)",
id->name,
rna_path.path.c_str());
return 0;
}
if (!adt->action) {
BKE_reportf(reports, RPT_ERROR, "No action to delete keyframes from for ID = %s", id->name);
return 0;
}
bAction *act = adt->action;
cfra = BKE_nla_tweakedit_remap(adt, cfra, NLATIME_CONVERT_UNMAP);
int array_index = rna_path.index.value_or(0);
int array_index_max = array_index + 1;
if (!rna_path.index.has_value()) {
array_index_max = RNA_property_array_length(&ptr, prop);
/* For single properties, increase max_index so that the property itself gets included,
* but don't do this for standard arrays since that can cause corruption issues
* (extra unused curves).
*/
if (array_index_max == array_index) {
array_index_max++;
}
}
Action &action = act->wrap();
Vector<FCurve *> modified_fcurves;
/* Just being defensive in the face of the NLA shenanigans above. This
* probably isn't necessary, but it doesn't hurt. */
BLI_assert(adt->action == act && action.slot_for_handle(adt->slot_handle) != nullptr);
Span<FCurve *> fcurves = fcurves_for_action_slot(action, adt->slot_handle);
/* This loop's clause is copied from the pre-existing code for legacy
* actions below, to ensure behavioral consistency between the two code
* paths. In the future when legacy actions are removed, we can restructure
* it to be clearer. */
for (; array_index < array_index_max; array_index++) {
FCurve *fcurve = fcurve_find(fcurves, {rna_path.path, array_index});
if (fcurve == nullptr) {
continue;
}
if (fcurve_delete_keyframe_at_time(fcurve, cfra)) {
modified_fcurves.append(fcurve);
}
}
if (!modified_fcurves.is_empty()) {
for (FCurve *fcurve : modified_fcurves) {
if (BKE_fcurve_is_empty(fcurve)) {
animdata_fcurve_delete(adt, fcurve);
}
}
deg_tag_after_keyframe_delete(bmain, id, adt);
}
return modified_fcurves.size();
}
/* ************************************************** */
/* KEYFRAME CLEAR */
int clear_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path)
{
AnimData *adt = BKE_animdata_from_id(id);
if (ELEM(nullptr, id, adt)) {
BKE_report(reports, RPT_ERROR, "No ID block and/or AnimData to delete keyframe from");
return 0;
}
PointerRNA ptr;
PropertyRNA *prop;
PointerRNA id_ptr = RNA_id_pointer_create(id);
if (RNA_path_resolve_property(&id_ptr, rna_path.path.c_str(), &ptr, &prop) == false) {
BKE_reportf(
reports,
RPT_ERROR,
"Could not clear keyframe, as RNA path is invalid for the given ID (ID = %s, path = %s)",
id->name,
rna_path.path.c_str());
return 0;
}
if (!adt->action) {
BKE_reportf(reports, RPT_ERROR, "No action to delete keyframes from for ID = %s", id->name);
return 0;
}
bAction *act = adt->action;
Action &action = act->wrap();
int key_count = 0;
if (adt->slot_handle) {
Vector<FCurve *> fcurves;
foreach_fcurve_in_action_slot_editable(action, adt->slot_handle, [&](FCurve &fcurve) {
if (rna_path.index.has_value() && rna_path.index.value() != fcurve.array_index) {
return;
}
if (rna_path.path != fcurve.rna_path) {
return;
}
fcurves.append(&fcurve);
});
for (FCurve *fcu : fcurves) {
if (action_fcurve_remove(action, *fcu)) {
key_count++;
}
}
}
if (key_count) {
deg_tag_after_keyframe_delete(bmain, id, adt);
}
return key_count;
}
struct KeyInsertData {
float2 position;
int array_index;
};
static SingleKeyingResult insert_key_layer(Main *bmain,
Action &action,
Layer &layer,
const Slot &slot,
const std::string &rna_path,
PropertyRNA *prop,
const std::optional<StringRefNull> channel_group,
const KeyInsertData &key_data,
const KeyframeSettings &key_settings,
const eInsertKeyFlags insert_key_flags)
{
assert_baklava_phase_1_invariants(layer);
BLI_assert(layer.strips().size() == 1);
const bool do_cyclic = (insert_key_flags & INSERTKEY_CYCLE_AWARE) && action.is_cyclic();
const PropertyType prop_type = RNA_property_type(prop);
const PropertySubType prop_subtype = RNA_property_subtype(prop);
Strip *strip = layer.strip(0);
return strip->data<StripKeyframeData>(action).keyframe_insert(
bmain,
slot,
{rna_path, key_data.array_index, prop_type, prop_subtype, channel_group},
key_data.position,
key_settings,
insert_key_flags,
do_cyclic ? std::optional(action.get_frame_range()) : std::nullopt);
}
static std::pair<Layer *, Slot *> prep_action_layer_for_keying(Action &action, ID &animated_id)
{
BLI_assert_msg(
ELEM(get_action(animated_id), &action, nullptr),
"The animated ID should not be using another Action than the one passed to this function");
Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
BLI_assert_msg(
slot,
"The conditions that would cause this Slot assignment to fail (such as the ID not being "
"animatible) should have been caught and handled by higher-level functions.");
action.layer_keystrip_ensure();
/* TODO: we currently assume this will always successfully find a layer.
* However, that may not be true in the future when we implement features like
* layer locking: if layers already exist, but they are all locked, then the
* default layer won't be added by the line above, but there also won't be any
* layers we can insert keys into. */
Layer *layer = action.get_layer_for_keyframing();
BLI_assert(layer != nullptr);
return std::make_pair(layer, slot);
}
static CombinedKeyingResult insert_key_layered_action(
Main *bmain,
Action &action,
Layer &layer,
const Slot &slot,
PropertyRNA *prop,
const std::optional<StringRefNull> channel_group,
const std::string &rna_path,
const float frame,
const Span<float> values,
const eInsertKeyFlags insert_key_flags,
const KeyframeSettings &key_settings,
const BitSpan keying_mask)
{
BLI_assert(bmain != nullptr);
int property_array_index = 0;
CombinedKeyingResult combined_result;
for (float value : values) {
if (!keying_mask[property_array_index]) {
combined_result.add(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK);
property_array_index++;
continue;
}
const KeyInsertData key_data = {{frame, value}, property_array_index};
const SingleKeyingResult result = insert_key_layer(bmain,
action,
layer,
slot,
rna_path,
prop,
channel_group,
key_data,
key_settings,
insert_key_flags);
combined_result.add(result);
property_array_index++;
}
return combined_result;
}
CombinedKeyingResult insert_keyframes(Main *bmain,
PointerRNA *struct_pointer,
const std::optional<StringRefNull> channel_group,
const Span<RNAPath> rna_paths,
const std::optional<float> scene_frame,
const AnimationEvalContext &anim_eval_context,
const eBezTriple_KeyframeType key_type,
const eInsertKeyFlags insert_key_flags)
{
ID *id = struct_pointer->owner_id;
PointerRNA id_pointer = RNA_id_pointer_create(id);
CombinedKeyingResult combined_result;
/* Init animdata if none available yet. */
AnimData *adt = BKE_animdata_ensure_id(id);
if (adt == nullptr) {
combined_result.add(SingleKeyingResult::ID_NOT_ANIMATABLE);
return combined_result;
}
if ((adt->action == nullptr) && (insert_key_flags & INSERTKEY_AVAILABLE)) {
combined_result.add(SingleKeyingResult::CANNOT_CREATE_FCURVE, rna_paths.size());
return combined_result;
}
if (const bAction *action = adt->action) {
if (ID_IS_LINKED(action) || ID_IS_OVERRIDE_LIBRARY(action)) {
combined_result.add(SingleKeyingResult::ID_NOT_EDITABLE, rna_paths.size());
return combined_result;
}
}
bAction *dna_action = id_action_ensure(bmain, id);
BLI_assert(dna_action != nullptr);
Action &action = dna_action->wrap();
KeyframeSettings key_settings = get_keyframe_settings(
(insert_key_flags & INSERTKEY_NO_USERPREF) == 0);
key_settings.keyframe_type = key_type;
/* NOTE: keyframing functions can deal with the nla_context being a nullptr. */
ListBaseT<NlaKeyframingContext> nla_cache = {nullptr, nullptr};
NlaKeyframingContext *nla_context = nullptr;
const float nla_frame = nla_time_remap(scene_frame.value_or(anim_eval_context.eval_time),
&anim_eval_context,
&id_pointer,
adt,
dna_action,
&nla_cache,
&nla_context);
const bool visual_keyframing = insert_key_flags & INSERTKEY_MATRIX;
auto [layer, slot] = prep_action_layer_for_keying(action, *struct_pointer->owner_id);
for (const RNAPath &rna_path : rna_paths) {
PointerRNA ptr;
PropertyRNA *prop = nullptr;
const bool path_resolved = RNA_path_resolve_property(
struct_pointer, rna_path.path.c_str(), &ptr, &prop);
if (!path_resolved) {
combined_result.add(SingleKeyingResult::CANNOT_RESOLVE_PATH);
continue;
}
Vector<float> rna_values = get_property_values(&ptr, prop, visual_keyframing);
BitVector<> rna_values_mask(rna_values.size(), false);
bool force_all;
/* NOTE: this function call is complex with interesting/non-obvious effects.
* Please see its documentation for details. */
BKE_animsys_nla_remap_keyframe_values(nla_context,
&ptr,
prop,
rna_values.as_mutable_span(),
rna_path.index.value_or(-1),
&anim_eval_context,
&force_all,
rna_values_mask);
std::optional<std::string> rna_path_id_to_prop = RNA_path_from_ID_to_property(&ptr, prop);
if (!rna_path_id_to_prop.has_value()) {
/* In the case of nested RNA properties the path cannot be reconstructed in all cases. There
* may be a system in place in the future, see #122427. */
if (struct_pointer->data != id) {
continue;
}
/* However if the struct pointer happens to be an ID pointer we can use the path that was
* passed in. This fixes issues like #132195. */
rna_path_id_to_prop = rna_path.path;
}
/* Handle the `force_all` condition mentioned above, ensuring the
* "all-or-nothing" behavior if needed.
*
* TODO: this currently doesn't account for the "Only Insert Available"
* flag, which also needs to be accounted for to actually ensure
* all-or-nothing behavior. This is because the function this part of the
* code originally came from (see #122053) also didn't account for it.
* Presumably that was an oversight, and should be addressed. But for now
* we're faithfully reproducing the original behavior.
*/
eInsertKeyFlags insert_key_flags_adjusted = insert_key_flags;
if (force_all && (insert_key_flags & (INSERTKEY_REPLACE | INSERTKEY_AVAILABLE))) {
/* Determine if at least one element would succeed getting keyed. */
bool at_least_one_would_succeed = false;
for (int i = 0; i < rna_values.size(); i++) {
const FCurve *fcu = fcurve_find_in_action(dna_action, {*rna_path_id_to_prop, i});
if (!fcu) {
continue;
}
/* We found an fcurve, and "Only Replace" is not on, so a key insertion
* would succeed according to the two flags we're accounting for. */
if (!(insert_key_flags & INSERTKEY_REPLACE)) {
at_least_one_would_succeed = true;
break;
}
/* "Only Replace" *is* on, so a key insertion would succeed only if we
* actually replace an existing keyframe. */
bool replace;
BKE_fcurve_bezt_binarysearch_index(fcu->bezt, nla_frame, fcu->totvert, &replace);
if (replace) {
at_least_one_would_succeed = true;
break;
}
}
/* If at least one would succeed, then we disable all keying flags that
* would prevent the other elements from getting keyed as well. */
if (at_least_one_would_succeed) {
insert_key_flags_adjusted &= ~(INSERTKEY_REPLACE | INSERTKEY_AVAILABLE);
}
}
CombinedKeyingResult result;
const std::optional<StringRefNull> this_rna_path_channel_group =
channel_group.has_value() ? *channel_group :
default_channel_group_for_path(&ptr, *rna_path_id_to_prop);
result = insert_key_layered_action(bmain,
action,
*layer,
*slot,
prop,
this_rna_path_channel_group,
*rna_path_id_to_prop,
nla_frame,
rna_values,
insert_key_flags,
key_settings,
rna_values_mask);
combined_result.merge(result);
}
BKE_animsys_free_nla_keyframing_context_cache(&nla_cache);
if (combined_result.get_count(SingleKeyingResult::SUCCESS) > 0) {
/* NOTE: this is NOT using ID_RECALC_ANIMATION on purpose, because that would be quite annoying
* in the following case:
*
* - Key Cube's loc/rot/scale.
* - Go to another frame.
* - Translate, rotate, and scale the cube.
* - Hover over the loc/rot/scale properties and one by one press 'I' to
* insert a key there.
*
* If ID_RECALC_ANIMATION were used, keying the location would immediately cause a flush of the
* animation data, popping the rotation and scale back to their animated values. */
DEG_id_tag_update(&dna_action->id, ID_RECALC_ANIMATION_NO_FLUSH);
/* TODO: it's not entirely clear why the action we got wouldn't be the same
* as the action in AnimData. Further, it's not clear why it would need to
* be tagged for a depsgraph update regardless. This code is here because it
* was part of the function this one was refactored from, but at some point
* this should be investigated and either documented or removed. */
if (!ELEM(adt->action, nullptr, dna_action)) {
DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
}
}
return combined_result;
}
} // namespace blender::animrig

View File

@@ -0,0 +1,353 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_fcurve.hh"
#include "BKE_scene.hh"
#include "DNA_scene_types.h"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_keyingsets.hh"
#include "WM_api.hh"
#include "WM_types.hh"
namespace blender::animrig {
static eInsertKeyFlags get_autokey_flags(const Scene *scene)
{
eInsertKeyFlags flag = INSERTKEY_NOFLAGS;
/* Visual keying. */
if (is_keying_flag(scene, KEYING_FLAG_VISUALKEY)) {
flag |= INSERTKEY_MATRIX;
}
/* Only needed. */
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTNEEDED)) {
flag |= INSERTKEY_NEEDED;
}
/* Only insert available. */
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTAVAILABLE)) {
flag |= INSERTKEY_AVAILABLE;
}
/* Keyframing mode - only replace existing keyframes. */
if (is_autokey_mode(scene, AUTOKEY_MODE_EDITKEYS)) {
flag |= INSERTKEY_REPLACE;
}
/* Cycle-aware keyframe insertion - preserve cycle period and flow. */
if (is_keying_flag(scene, KEYING_FLAG_CYCLEAWARE)) {
flag |= INSERTKEY_CYCLE_AWARE;
}
return flag;
}
bool is_autokey_on(const Scene *scene)
{
if (scene) {
return scene->toolsettings->autokey_mode & AUTOKEY_ON;
}
return U.autokey_mode & AUTOKEY_ON;
}
bool is_autokey_mode(const Scene *scene, const eAutokey_Mode mode)
{
if (scene) {
return scene->toolsettings->autokey_mode == mode;
}
return U.autokey_mode == mode;
}
bool autokeyframe_cfra_can_key(const Scene *scene, ID *id)
{
/* Only filter if auto-key mode requires this. */
if (!is_autokey_on(scene)) {
return false;
}
if (is_autokey_mode(scene, AUTOKEY_MODE_EDITKEYS)) {
/* Replace Mode:
* For whole block, only key if there's a keyframe on that frame already
* This is a valid assumption when we're blocking + tweaking
*/
const float cfra = BKE_scene_frame_get(scene);
return id_frame_has_keyframe(id, cfra);
}
/* Normal Mode (or treat as being normal mode):
*
* Just in case the flags aren't set properly (i.e. only on/off is set, without a mode)
* let's set the "normal" flag too, so that it will all be sane everywhere...
*/
scene->toolsettings->autokey_mode = AUTOKEY_MODE_NORMAL;
return true;
}
void autokeyframe_object(bContext *C, const Scene *scene, Object *ob, Span<RNAPath> rna_paths)
{
BLI_assert(ob != nullptr);
BLI_assert(scene != nullptr);
BLI_assert(C != nullptr);
ID *id = &ob->id;
if (!autokeyframe_cfra_can_key(scene, id)) {
return;
}
ReportList *reports = CTX_wm_reports(C);
KeyingSet *active_ks = scene_get_active_keyingset(scene);
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(
depsgraph, BKE_scene_frame_get(scene));
/* Get flags used for inserting keyframes. */
const eInsertKeyFlags flag = get_autokey_flags(scene);
/* Add data-source override for the object. */
Vector<PointerRNA> sources;
relative_keyingset_add_source(sources, id);
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (active_ks)) {
/* Only insert into active keyingset
* NOTE: we assume here that the active Keying Set
* does not need to have its iterator overridden.
*/
apply_keyingset(C, &sources, active_ks, ModifyKeyMode::INSERT, anim_eval_context.eval_time);
return;
}
const float scene_frame = BKE_scene_frame_get(scene);
Main *bmain = CTX_data_main(C);
CombinedKeyingResult combined_result;
for (PointerRNA ptr : sources) {
const CombinedKeyingResult result = insert_keyframes(
bmain,
&ptr,
std::nullopt,
rna_paths,
scene_frame,
anim_eval_context,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
flag);
combined_result.merge(result);
}
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
combined_result.generate_reports(reports);
}
}
bool autokeyframe_object(bContext *C, Scene *scene, Object *ob, KeyingSet *ks)
{
if (!autokeyframe_cfra_can_key(scene, &ob->id)) {
return false;
}
/* Now insert the key-frame(s) using the Keying Set:
* 1) Add data-source override for the Object.
* 2) Insert key-frames.
* 3) Free the extra info.
*/
Vector<PointerRNA> sources;
relative_keyingset_add_source(sources, &ob->id);
apply_keyingset(C, &sources, ks, ModifyKeyMode::INSERT, BKE_scene_frame_get(scene));
return true;
}
bool autokeyframe_pchan(bContext *C, Scene *scene, Object *ob, bPoseChannel *pchan, KeyingSet *ks)
{
if (!autokeyframe_cfra_can_key(scene, &ob->id)) {
return false;
}
/* Now insert the keyframe(s) using the Keying Set:
* 1) Add data-source override for the pose-channel.
* 2) Insert key-frames.
* 3) Free the extra info.
*/
Vector<PointerRNA> sources;
relative_keyingset_add_source(sources, &ob->id, RNA_PoseBone, pchan);
apply_keyingset(C, &sources, ks, ModifyKeyMode::INSERT, BKE_scene_frame_get(scene));
return true;
}
void autokeyframe_pose_channel(bContext *C,
Scene *scene,
Object *ob,
bPoseChannel *pose_channel,
Span<RNAPath> rna_paths,
short targetless_ik)
{
BLI_assert(C != nullptr);
BLI_assert(scene != nullptr);
BLI_assert(ob != nullptr);
BLI_assert(pose_channel != nullptr);
Main *bmain = CTX_data_main(C);
ID *id = &ob->id;
if (!animrig::autokeyframe_cfra_can_key(scene, id)) {
return;
}
ReportList *reports = CTX_wm_reports(C);
KeyingSet *active_ks = scene_get_active_keyingset(scene);
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
const float scene_frame = BKE_scene_frame_get(scene);
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
scene_frame);
/* flag is initialized from UserPref keyframing settings
* - special exception for targetless IK - INSERTKEY_MATRIX keyframes should get
* visual keyframes even if flag not set, as it's not that useful otherwise
* (for quick animation recording)
*/
eInsertKeyFlags flag = get_autokey_flags(scene);
if (targetless_ik) {
flag |= INSERTKEY_MATRIX;
}
Vector<PointerRNA> sources;
/* Add data-source override for the camera object. */
relative_keyingset_add_source(sources, id, RNA_PoseBone, pose_channel);
/* only insert into active keyingset? */
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (active_ks)) {
/* Run the active Keying Set on the current data-source. */
apply_keyingset(C, &sources, active_ks, ModifyKeyMode::INSERT, anim_eval_context.eval_time);
return;
}
CombinedKeyingResult combined_result;
for (PointerRNA &ptr : sources) {
const CombinedKeyingResult result = insert_keyframes(
bmain,
&ptr,
std::nullopt,
rna_paths,
scene_frame,
anim_eval_context,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
flag);
combined_result.merge(result);
}
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
combined_result.generate_reports(reports);
}
}
bool autokeyframe_property(bContext *C,
Scene *scene,
PointerRNA *ptr,
PropertyRNA *prop,
const int rnaindex,
const float cfra,
const bool only_if_property_keyed)
{
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
cfra);
bAction *action;
bool driven;
bool special;
/* For entire array buttons we check the first component, it's not perfect
* but works well enough in typical cases. */
const int rnaindex_check = (rnaindex == -1) ? 0 : rnaindex;
FCurve *fcu = BKE_fcurve_find_by_rna_context_ui(
C, ptr, prop, rnaindex_check, nullptr, &action, &driven, &special);
/* Only early out when we actually want an existing F-curve already
* (e.g. auto-keyframing from buttons). */
if (fcu == nullptr && (driven || special || only_if_property_keyed)) {
return false;
}
if (driven) {
return false;
}
bool changed = false;
if (special) {
/* NLA Strip property. */
if (is_autokey_on(scene)) {
ReportList *reports = CTX_wm_reports(C);
ToolSettings *ts = scene->toolsettings;
const SingleKeyingResult result = insert_keyframe_direct(
*ptr,
*prop,
*fcu,
anim_eval_context.eval_time,
eBezTriple_KeyframeType(ts->keyframe_type),
eInsertKeyFlags(0));
changed = result == SingleKeyingResult::SUCCESS;
if (result != SingleKeyingResult::SUCCESS) {
generate_single_keying_result_report(result, reports);
}
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr);
}
}
else {
ID *id = ptr->owner_id;
Main *bmain = CTX_data_main(C);
/* TODO: this should probably respect the keyingset only option for anim */
if (autokeyframe_cfra_can_key(scene, id)) {
ToolSettings *ts = scene->toolsettings;
const eInsertKeyFlags flag = get_autokey_flags(scene);
if (only_if_property_keyed) {
/* NOTE: We use rnaindex instead of fcu->array_index,
* because a button may control all items of an array at once.
* E.g., color wheels (see #42567). */
BLI_assert((fcu->array_index == rnaindex) || (rnaindex == -1));
}
const std::optional<std::string> group = (fcu && fcu->grp) ? std::optional(fcu->grp->name) :
std::nullopt;
const std::string path = fcu ? fcu->rna_path :
RNA_path_from_ID_to_property(ptr, prop).value_or("");
/* NOTE: `rnaindex == -1` is a magic number, meaning either "operate on
* all elements" or "not an array property". */
const std::optional<int> array_index = rnaindex < 0 ? std::nullopt : std::optional(rnaindex);
PointerRNA id_pointer = RNA_id_pointer_create(ptr->owner_id);
CombinedKeyingResult result = insert_keyframes(bmain,
&id_pointer,
group,
{{path, {}, array_index}},
std::nullopt,
anim_eval_context,
eBezTriple_KeyframeType(ts->keyframe_type),
flag);
changed = result.get_count(SingleKeyingResult::SUCCESS) != 0;
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr);
}
}
return changed;
}
} // namespace blender::animrig

View File

@@ -0,0 +1,973 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_nla.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_armature.hh"
#include "BKE_fcurve.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_nla.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "DNA_material_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
class KeyframingTest : public bke::BlenderGTestBase {
public:
Main *bmain;
/* For standard single-action testing. */
Object *object;
PointerRNA object_rna_pointer;
/* For pose bone single-action testing. */
Object *armature_object;
bArmature *armature;
PointerRNA armature_object_rna_pointer;
/* For NLA testing. */
Object *object_with_nla;
PointerRNA object_with_nla_rna_pointer;
bAction *nla_action;
/* For action reuse testing. */
Object *cube;
PointerRNA cube_rna_pointer;
Mesh *cube_mesh;
PointerRNA cube_mesh_rna_pointer;
Material *material;
PointerRNA material_rna_pointer;
void SetUp() override
{
bmain = BKE_main_new();
object = BKE_object_add_only_object(bmain, OB_EMPTY, "Empty");
object_rna_pointer = RNA_id_pointer_create(&object->id);
Bone *bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "Bone");
armature = BKE_armature_add(bmain, "Armature");
BLI_addtail(&armature->bonebase, bone);
armature_object = BKE_object_add_only_object(bmain, OB_ARMATURE, "Armature");
armature_object->data = id_cast<ID *>(armature);
BKE_pose_ensure(bmain, armature_object, armature, false);
armature_object_rna_pointer = RNA_id_pointer_create(&armature_object->id);
cube = BKE_object_add_only_object(bmain, OB_MESH, "cube");
cube_rna_pointer = RNA_id_pointer_create(&cube->id);
cube_mesh = BKE_mesh_add(bmain, "cube_mesh");
cube_mesh_rna_pointer = RNA_id_pointer_create(&cube_mesh->id);
/* Removing the implicit id user. Using BKE_mesh_assign_object increments the user count which
* would leave it at 2 otherwise. */
id_us_min(&cube_mesh->id);
BKE_mesh_assign_object(bmain, cube, cube_mesh);
material = BKE_material_add(bmain, "material");
material_rna_pointer = RNA_id_pointer_create(&material->id);
id_us_min(&material->id);
BKE_object_material_assign(bmain, cube, material, 0, BKE_MAT_ASSIGN_OBDATA);
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 = BKE_id_new<bAction>(bmain, "NLAAction");
/* Set up an NLA system with a single NLA track with a single offset-in-time
* NLA strip, and make that strip active and in tweak mode. */
AnimData *adt = BKE_animdata_ensure_id(&object_with_nla->id);
NlaTrack *track = BKE_nlatrack_new_head(&adt->nla_tracks, false);
ASSERT_NE(track, nullptr);
NlaStrip *strip = BKE_nlastrip_new(nla_action, object_with_nla->id);
BKE_nlatrack_add_strip(track, strip, false);
ASSERT_NE(strip, nullptr);
ASSERT_TRUE(animrig::nla::assign_action(*strip, nla_action->wrap(), object_with_nla->id));
track->flag |= NLATRACK_ACTIVE;
strip->flag |= NLASTRIP_FLAG_ACTIVE;
strip->start = -10.0;
strip->end = 990.0;
strip->actstart = 0.0;
strip->actend = 1000.0;
strip->scale = 1.0;
strip->blendmode = NLASTRIP_MODE_COMBINE;
}
void TearDown() override
{
BKE_main_free(bmain);
}
Channelbag *get_channelbag_in_first_layer(Object &object)
{
Action &action = object.adt->action->wrap();
if (action.layer_array_num == 0) {
return nullptr;
}
Layer *layer = action.layer(0);
if (layer->strip_array_num == 0) {
return nullptr;
}
Strip *strip = layer->strip(0);
BLI_assert(strip->type() == Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(action);
return strip_data.channelbag_for_slot(object.adt->slot_handle);
}
};
/* ------------------------------------------------------------
* Tests for `insert_keyframes()` with layered actions.
*/
/* Keying a non-array property. */
TEST_F(KeyframingTest, insert_keyframes__non_array_property)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First time should create:
* - AnimData
* - Action
* - Slot
* - Layer
* - Infinite KeyframeStrip
* - FCurve with a single key
*/
object->empty_drawsize = 42.0;
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"empty_display_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
/* The action has a slot, it's named properly, and it's correctly assigned
* to the object. */
ASSERT_EQ(1, action.slots().size());
Slot *slot = action.slot(0);
EXPECT_STREQ(object->id.name, slot->identifier);
EXPECT_STREQ(object->adt->last_slot_identifier, slot->identifier);
EXPECT_EQ(object->adt->slot_handle, slot->handle);
/* We have the default layer and strip. */
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
EXPECT_TRUE(strlen(action.layer(0)->name) > 0);
Strip *strip = action.layer(0)->strip(0);
ASSERT_TRUE(strip->is_infinite());
ASSERT_EQ(Strip::Type::Keyframe, strip->type());
StripKeyframeData *strip_data = &strip->data<StripKeyframeData>(action);
/* We have a channel bag for the slot. */
Channelbag *channelbag = strip_data->channelbag_for_slot(*slot);
ASSERT_NE(nullptr, channelbag);
/* The fcurves in the channel bag are what we expect. */
EXPECT_EQ(1, channelbag->fcurves().size());
const FCurve *fcurve = channelbag->fcurve_find({"empty_display_size", 0});
ASSERT_NE(nullptr, fcurve);
ASSERT_NE(nullptr, fcurve->bezt);
EXPECT_EQ(1, fcurve->totvert);
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
EXPECT_EQ(42.0, fcurve->bezt[0].vec[1][1]);
/* Second time inserting with a different value on the same frame should
* simply replace the key. */
object->empty_drawsize = 86.0;
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"empty_display_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(1, fcurve->totvert);
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
EXPECT_EQ(86.0, fcurve->bezt[0].vec[1][1]);
/* Third time inserting on a different time should add a second key. */
object->empty_drawsize = 7.0;
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"empty_display_size"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_3.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(2, fcurve->totvert);
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
EXPECT_EQ(86.0, fcurve->bezt[0].vec[1][1]);
EXPECT_EQ(10.0, fcurve->bezt[1].vec[1][0]);
EXPECT_EQ(7.0, fcurve->bezt[1].vec[1][1]);
}
TEST_F(KeyframingTest, insert_keyframes__action_reuse)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
CombinedKeyingResult result_ob;
result_ob = insert_keyframes(bmain,
&armature_object_rna_pointer,
std::nullopt,
{{"location"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
ASSERT_TRUE(armature_object->adt != nullptr);
ASSERT_TRUE(armature_object->adt->action != nullptr);
PointerRNA armature_rna_pointer = RNA_id_pointer_create(&armature->id);
result_ob = insert_keyframes(bmain,
&armature_rna_pointer,
std::nullopt,
{{"display_type"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
ASSERT_TRUE(armature->adt != nullptr);
ASSERT_TRUE(armature->adt->action != nullptr);
/* Action is expected to be reused between object and data. */
ASSERT_EQ(armature->adt->action, armature_object->adt->action);
Action &action = armature->adt->action->wrap();
/* Should have two slots now. */
ASSERT_EQ(action.slot_array_num, 2);
for (Slot *slot : action.slots()) {
ASSERT_TRUE(slot->idtype == ID_AR || slot->idtype == ID_OB);
}
}
TEST_F(KeyframingTest, insert_keyframes__action_reuse_material)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
CombinedKeyingResult result_ob;
result_ob = insert_keyframes(bmain,
&material_rna_pointer,
std::nullopt,
{{"pass_index"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
ASSERT_TRUE(material->adt != nullptr);
ASSERT_TRUE(material->adt->action != nullptr);
result_ob = insert_keyframes(bmain,
&cube_rna_pointer,
std::nullopt,
{{"location"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
ASSERT_TRUE(cube->adt != nullptr);
ASSERT_TRUE(cube->adt->action != nullptr);
/* Actions are not shared between object and material. */
ASSERT_NE(cube->adt->action, material->adt->action);
result_ob = insert_keyframes(bmain,
&cube_mesh_rna_pointer,
std::nullopt,
{{"remesh_voxel_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
ASSERT_TRUE(cube_mesh->adt != nullptr);
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
/* Reuse between Object and object data. */
ASSERT_EQ(cube_mesh->adt->action, cube->adt->action);
/* Still no reuse from mesh to material. */
ASSERT_NE(cube_mesh->adt->action, material->adt->action);
Action &action = cube->adt->action->wrap();
/* Should have two slots now. */
ASSERT_EQ(action.slot_array_num, 2);
/* Material action should have only 1 slot. */
ASSERT_EQ(material->adt->action->wrap().slot_array_num, 1);
for (Slot *slot : action.slots()) {
ASSERT_TRUE(slot->idtype == ID_ME || slot->idtype == ID_OB);
ASSERT_NE(slot->idtype, ID_MA);
}
}
TEST_F(KeyframingTest, insert_keyframes__action_reuse_multiuser)
{
Object *another_object = BKE_object_add_only_object(bmain, OB_MESH, "another_object");
PointerRNA another_object_rna_pointer = RNA_id_pointer_create(&another_object->id);
BKE_mesh_assign_object(bmain, another_object, cube_mesh);
ASSERT_EQ(ID_REFCOUNTING_USERS(&cube_mesh->id), 2);
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
CombinedKeyingResult result_ob;
result_ob = insert_keyframes(bmain,
&cube_rna_pointer,
std::nullopt,
{{"location"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
ASSERT_TRUE(cube->adt != nullptr);
ASSERT_TRUE(cube->adt->action != nullptr);
result_ob = insert_keyframes(bmain,
&cube_mesh_rna_pointer,
std::nullopt,
{{"remesh_voxel_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
ASSERT_TRUE(cube_mesh->adt != nullptr);
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
/* When an ID is used more than once, the action should not be reused. */
ASSERT_NE(cube->adt->action, cube_mesh->adt->action);
result_ob = insert_keyframes(bmain,
&another_object_rna_pointer,
std::nullopt,
{{"location"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
ASSERT_TRUE(another_object->adt != nullptr);
ASSERT_TRUE(another_object->adt->action != nullptr);
/* Given that those two objects are connected by a mesh (which due to this has two users) the
* action shouldn't be reused between them. */
ASSERT_NE(cube->adt->action, another_object->adt->action);
}
/* Keying a single element of an array property. */
TEST_F(KeyframingTest, insert_keyframes__single_element)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler", std::nullopt, 0}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
EXPECT_EQ(1, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
}
/* Keying all elements of an array property. */
TEST_F(KeyframingTest, insert_keyframes__all_elements)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(3, result.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
EXPECT_EQ(3, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 1}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
}
/* Keying a pose bone from its own RNA pointer. */
TEST_F(KeyframingTest, insert_keyframes__pose_bone_rna_pointer)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
bPoseChannel *pchan = BKE_pose_channel_find_name(armature_object->pose, "Bone");
PointerRNA pose_bone_rna_pointer = RNA_pointer_create_discrete(
&armature_object->id, RNA_PoseBone, pchan);
const CombinedKeyingResult result = insert_keyframes(bmain,
&pose_bone_rna_pointer,
std::nullopt,
{{"rotation_euler", std::nullopt, 0}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, armature_object->adt);
ASSERT_NE(nullptr, armature_object->adt->action);
Action &action = armature_object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
EXPECT_EQ(1, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"pose.bones[\"Bone\"].rotation_euler", 0}));
}
/* Keying a pose bone from its owning ID's RNA pointer. */
TEST_F(KeyframingTest, insert_keyframes__pose_bone_owner_id_pointer)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
const CombinedKeyingResult result = insert_keyframes(
bmain,
&armature_object_rna_pointer,
std::nullopt,
{{"pose.bones[\"Bone\"].rotation_euler", std::nullopt, 0}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, armature_object->adt);
ASSERT_NE(nullptr, armature_object->adt->action);
Action &action = armature_object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
EXPECT_EQ(1, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"pose.bones[\"Bone\"].rotation_euler", 0}));
}
/* Keying multiple elements of multiple properties at once. */
TEST_F(KeyframingTest, insert_keyframes__multiple_properties)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{
{"empty_display_size"},
{"location"},
{"rotation_euler", std::nullopt, 0},
{"rotation_euler", std::nullopt, 2},
},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(6, result.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
EXPECT_EQ(6, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"empty_display_size", 0}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 0}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 1}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 2}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
}
/* Keying more than one ID on the same action. */
TEST_F(KeyframingTest, insert_keyframes__multiple_ids)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First object should crate the action and get a slot and channel bag. */
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"empty_display_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
/* The action has a slot and it's assigned to the first object. */
ASSERT_EQ(1, action.slots().size());
Slot *slot_1 = action.slot_for_handle(object->adt->slot_handle);
ASSERT_NE(nullptr, slot_1);
EXPECT_STREQ(object->id.name, slot_1->identifier);
EXPECT_STREQ(object->adt->last_slot_identifier, slot_1->identifier);
/* Get the keyframe strip. */
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
/* We have a single channel bag, and it's for the first object's slot. */
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag_1 = strip_data->channelbag_for_slot(*slot_1);
ASSERT_NE(nullptr, channelbag_1);
/* Assign the action to the second object, with no slot. */
ASSERT_TRUE(assign_action(&action, armature_object->id));
ASSERT_EQ(assign_action_slot(nullptr, armature_object->id), ActionSlotAssignmentResult::OK);
/* Keying the second object should go into the same action, creating a new
* slot and channel bag. */
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&armature_object_rna_pointer,
std::nullopt,
{{"empty_display_size"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
ASSERT_EQ(2, action.slots().size());
Slot *slot_2 = action.slot_for_handle(armature_object->adt->slot_handle);
ASSERT_NE(nullptr, slot_2);
EXPECT_STREQ(armature_object->id.name, slot_2->identifier);
EXPECT_STREQ(armature_object->adt->last_slot_identifier, slot_2->identifier);
ASSERT_EQ(2, strip_data->channelbags().size());
Channelbag *channelbag_2 = strip_data->channelbag_for_slot(*slot_2);
ASSERT_NE(nullptr, channelbag_2);
}
/* Keying with the "Only Insert Available" flag. */
TEST_F(KeyframingTest, insert_keyframes__only_available)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First attempt should fail, because there are no fcurves yet. */
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_AVAILABLE);
EXPECT_EQ(0, result_1.get_count(SingleKeyingResult::SUCCESS));
/* It's unclear why an AnimData should be created if keying fails
* here. It may even be undesirable. This check is just here to ensure no
* *unintentional* changes in behavior. */
ASSERT_NE(nullptr, object->adt);
/* No action is created when using the flag INSERTKEY_AVAILABLE on an
* object without an action. */
ASSERT_EQ(nullptr, object->adt->action);
/* Insert a key on two of the elements without using the flag so that there
* will be two fcurves. */
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{
{"rotation_euler", std::nullopt, 0},
{"rotation_euler", std::nullopt, 2},
},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
/* If an action is created, it should be the default action with one
* layer and an infinite keyframe strip. */
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
EXPECT_EQ(object->adt->slot_handle, action.slot(0)->handle);
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
/* Second attempt should succeed with two keys, because two of the elements
* now have fcurves. */
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_AVAILABLE);
EXPECT_EQ(2, result_3.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(2, channelbag->fcurves().size());
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
}
/* Keying with the "Only Replace" flag. */
TEST_F(KeyframingTest, insert_keyframes__only_replace)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First attempt should fail, because there are no fcurves yet. */
object->rot[0] = 42.0;
object->rot[1] = 42.0;
object->rot[2] = 42.0;
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_REPLACE);
EXPECT_EQ(0, result_1.get_count(SingleKeyingResult::SUCCESS));
/* Insert a key for two of the elements so that there will be two fcurves with
* one key each. */
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{
{"rotation_euler", std::nullopt, 0},
{"rotation_euler", std::nullopt, 2},
},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
ASSERT_EQ(2, channelbag->fcurves().size());
const FCurve *fcurve_x = channelbag->fcurve_find({"rotation_euler", 0});
const FCurve *fcurve_z = channelbag->fcurve_find({"rotation_euler", 2});
EXPECT_EQ(1, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_z->totvert);
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
EXPECT_EQ(42.0, fcurve_x->bezt[0].vec[1][1]);
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
EXPECT_EQ(42.0, fcurve_z->bezt[0].vec[1][1]);
/* Second attempt should also fail, because we insert on a different frame
* than the two keys we just created. */
object->rot[0] = 86.0;
object->rot[1] = 86.0;
object->rot[2] = 86.0;
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
5.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_REPLACE);
EXPECT_EQ(0, result_3.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(2, channelbag->fcurves().size());
EXPECT_EQ(1, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_z->totvert);
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
EXPECT_EQ(42.0, fcurve_x->bezt[0].vec[1][1]);
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
EXPECT_EQ(42.0, fcurve_z->bezt[0].vec[1][1]);
/* The third attempt, keying on the original frame, should succeed and replace
* the existing key on each fcurve. */
const CombinedKeyingResult result_4 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_REPLACE);
EXPECT_EQ(2, result_4.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(2, channelbag->fcurves().size());
EXPECT_EQ(1, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_z->totvert);
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
EXPECT_EQ(86.0, fcurve_x->bezt[0].vec[1][1]);
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
EXPECT_EQ(86.0, fcurve_z->bezt[0].vec[1][1]);
}
/* Keying with the "Only Insert Needed" flag. */
TEST_F(KeyframingTest, insert_keyframes__only_needed)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* First attempt should succeed, because there are no fcurves yet. */
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NEEDED);
EXPECT_EQ(3, result_1.get_count(SingleKeyingResult::SUCCESS));
ASSERT_NE(nullptr, object->adt);
ASSERT_NE(nullptr, object->adt->action);
Action &action = object->adt->action->wrap();
ASSERT_EQ(1, action.slots().size());
ASSERT_EQ(1, action.layers().size());
ASSERT_EQ(1, action.layer(0)->strips().size());
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
ASSERT_EQ(1, strip_data->channelbags().size());
Channelbag *channelbag = strip_data->channelbag(0);
ASSERT_EQ(3, channelbag->fcurves().size());
const FCurve *fcurve_x = channelbag->fcurve_find({"rotation_euler", 0});
const FCurve *fcurve_y = channelbag->fcurve_find({"rotation_euler", 1});
const FCurve *fcurve_z = channelbag->fcurve_find({"rotation_euler", 2});
EXPECT_EQ(1, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_y->totvert);
EXPECT_EQ(1, fcurve_z->totvert);
/* Second attempt should fail, because there is now an fcurve for the
* property, but its value matches the current property value. */
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NEEDED);
EXPECT_EQ(0, result_2.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(3, channelbag->fcurves().size());
EXPECT_EQ(1, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_y->totvert);
EXPECT_EQ(1, fcurve_z->totvert);
/* Third attempt should succeed on two elements, because we change the value
* of those elements to differ from the existing fcurves. */
object->rot[0] = 123.0;
object->rot[2] = 123.0;
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_euler"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NEEDED);
EXPECT_EQ(2, result_3.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(3, channelbag->fcurves().size());
EXPECT_EQ(2, fcurve_x->totvert);
EXPECT_EQ(1, fcurve_y->totvert);
EXPECT_EQ(2, fcurve_z->totvert);
}
/* Passing the frame number explicitly vs not. */
TEST_F(KeyframingTest, insert_keyframes__optional_frame)
{
/* If the frame number is not explicitly passed, the eval frame from the
* animation evaluation context should be used. */
AnimationEvalContext anim_eval_context = {nullptr, 5.0};
object->rotmode = ROT_MODE_XYZ;
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_mode"}},
std::nullopt,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
Channelbag *channelbag = get_channelbag_in_first_layer(*object);
FCurve *fcurve = channelbag->fcurve_find({"rotation_mode", 0});
EXPECT_EQ(5.0, fcurve->bezt[0].vec[1][0]);
EXPECT_EQ(float(ROT_MODE_XYZ), fcurve->bezt[0].vec[1][1]);
/* If the frame number *is* explicitly passed, it should be used. */
object->rotmode = ROT_MODE_QUAT;
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
&object_rna_pointer,
std::nullopt,
{{"rotation_mode"}},
10.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
EXPECT_EQ(5.0, fcurve->bezt[0].vec[1][0]);
EXPECT_EQ(float(ROT_MODE_XYZ), fcurve->bezt[0].vec[1][1]);
EXPECT_EQ(10.0, fcurve->bezt[1].vec[1][0]);
EXPECT_EQ(float(ROT_MODE_QUAT), fcurve->bezt[1].vec[1][1]);
}
/* Passing the channel group explicitly vs not. */
TEST_F(KeyframingTest, insert_keyframes__optional_channel_group)
{
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
/* If the channel group is not explicitly passed, the default should be used. */
const CombinedKeyingResult result_1 = insert_keyframes(
bmain,
&object_rna_pointer,
std::nullopt,
{{"location", std::nullopt, 0}, {"visible_shadow"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(2, result_1.get_count(SingleKeyingResult::SUCCESS));
Channelbag *channelbag = get_channelbag_in_first_layer(*object);
/* Location X should get the default transform group. */
FCurve *fcurve_location_x = channelbag->fcurve_find({"location", 0});
ASSERT_NE(nullptr, fcurve_location_x->grp);
EXPECT_EQ(0, strcmp("Object Transforms", fcurve_location_x->grp->name));
/* Shadow visibility should get no group. */
FCurve *fcurve_visible_shadow = channelbag->fcurve_find({"visible_shadow", 0});
ASSERT_EQ(nullptr, fcurve_visible_shadow->grp);
/* If the channel group *is* explicitly passed, it should override the default. */
const CombinedKeyingResult result_2 = insert_keyframes(
bmain,
&object_rna_pointer,
"Foo",
{{"location", std::nullopt, 1}, {"hide_render"}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
/* Both location Y and render visibility should get the "Foo" group. */
FCurve *fcurve_location_y = channelbag->fcurve_find({"location", 1});
ASSERT_NE(nullptr, fcurve_location_y->grp);
EXPECT_EQ(0, strcmp("Foo", fcurve_location_y->grp->name));
FCurve *fcurve_hide_render = channelbag->fcurve_find({"hide_render", 0});
ASSERT_NE(nullptr, fcurve_hide_render->grp);
EXPECT_EQ(0, strcmp("Foo", fcurve_hide_render->grp->name));
}
/* Inserting a key into an NLA strip that has a time offset should remap the
* key's time to the local time of the strip. */
TEST_F(KeyframingTest, insert_keyframes__nla_time_remapping)
{
BKE_nla_tweakmode_enter({object_with_nla->id, *object_with_nla->adt});
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
const CombinedKeyingResult result = insert_keyframes(bmain,
&object_with_nla_rna_pointer,
std::nullopt,
{{"location", std::nullopt, 0}},
1.0,
anim_eval_context,
BEZT_KEYTYPE_KEYFRAME,
INSERTKEY_NOFLAGS);
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
Action &act = nla_action->wrap();
Layer *layer = act.layer(0);
Strip *strip = layer->strip(0);
BLI_assert(strip->type() == Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(act);
Channelbag *channelbag = strip_data.channelbag_for_slot(nla_action->wrap().slots()[0]->handle);
EXPECT_EQ(1, channelbag->fcurve_array_num);
FCurve *fcurve = channelbag->fcurve_find({"location", 0});
ASSERT_NE(nullptr, fcurve);
ASSERT_NE(nullptr, fcurve->bezt);
EXPECT_EQ(1, fcurve->totvert);
EXPECT_EQ(11.0, fcurve->bezt[0].vec[1][0]);
BKE_nla_tweakmode_exit({object_with_nla->id, *object_with_nla->adt});
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,469 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_keyframing.hh"
#include "ANIM_keyingsets.hh"
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "DEG_depsgraph.hh"
#include "DNA_anim_types.h"
#include "DNA_scene_types.h"
#include "RNA_access.hh"
#include "WM_api.hh"
namespace blender {
/* Keying Set Type Info declarations. */
static ListBaseT<KeyingSetInfo> keyingset_type_infos = {nullptr, nullptr};
ListBaseT<KeyingSet> builtin_keyingsets = {nullptr, nullptr};
namespace animrig {
void keyingset_info_register(KeyingSetInfo *keyingset_info)
{
/* Create a new KeyingSet
* - inherit name and keyframing settings from the typeinfo
*/
KeyingSet *keyingset = BKE_keyingset_add(&builtin_keyingsets,
keyingset_info->idname,
keyingset_info->name,
eKS_Settings{},
keyingset_info->keyingflag);
/* Link this KeyingSet with its typeinfo. */
memcpy(&keyingset->typeinfo, keyingset_info->idname, sizeof(keyingset->typeinfo));
/* Copy description. */
STRNCPY(keyingset->description, keyingset_info->description);
/* Add type-info to the list. */
BLI_addtail(&keyingset_type_infos, keyingset_info);
}
void keyingset_info_unregister(Main *bmain, KeyingSetInfo *keyingset_info)
{
/* Find relevant builtin KeyingSets which use this, and remove them. */
/* TODO: this isn't done now, since unregister is really only used at the moment when we
* reload the scripts, which kind of defeats the purpose of "builtin"? */
for (KeyingSet &keyingset : builtin_keyingsets.items_mutable()) {
/* Remove if matching typeinfo name. */
if (!STREQ(keyingset.typeinfo, keyingset_info->idname)) {
continue;
}
Scene *scene;
BKE_keyingset_free_paths(&keyingset);
BLI_remlink(&builtin_keyingsets, &keyingset);
for (scene = static_cast<Scene *>(bmain->scenes.first); scene;
scene = static_cast<Scene *>(scene->id.next))
{
BLI_remlink_safe(&scene->keyingsets, &keyingset);
}
MEM_delete(&keyingset);
}
BLI_freelinkN(&keyingset_type_infos, keyingset_info);
}
void keyingset_infos_exit()
{
/* Free type infos. */
for (KeyingSetInfo &keyingset_info : keyingset_type_infos.items_mutable()) {
/* Free extra RNA data, and remove from list. */
if (keyingset_info.rna_ext.free) {
keyingset_info.rna_ext.free(keyingset_info.rna_ext.data);
}
BLI_freelinkN(&keyingset_type_infos, &keyingset_info);
}
BKE_keyingsets_free(&builtin_keyingsets);
}
bool keyingset_find_id(KeyingSet *keyingset, ID *id)
{
if (ELEM(nullptr, keyingset, id)) {
return false;
}
return BLI_findptr(&keyingset->paths, id, offsetof(KS_Path, id)) != nullptr;
}
KeyingSetInfo *keyingset_info_find_name(const char name[])
{
if ((name == nullptr) || (name[0] == 0)) {
return nullptr;
}
/* Search by comparing names. */
return static_cast<KeyingSetInfo *>(
BLI_findstring(&keyingset_type_infos, name, offsetof(KeyingSetInfo, idname)));
}
KeyingSet *builtin_keyingset_get_named(const char name[])
{
if (name[0] == 0) {
return nullptr;
}
/* Loop over KeyingSets checking names. */
for (KeyingSet &keyingset : builtin_keyingsets) {
if (STREQ(name, keyingset.idname)) {
return &keyingset;
}
}
/* Complain about missing keying sets on debug builds. */
#ifndef NDEBUG
printf("%s: '%s' not found\n", __func__, name);
#endif
return nullptr;
}
KeyingSet *get_keyingset_for_autokeying(const Scene *scene, const char *transformKSName)
{
/* Get KeyingSet to use
* - use the active KeyingSet if defined (and user wants to use it for all autokeying),
* or otherwise key transforms only
*/
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (scene->active_keyingset)) {
return scene_get_active_keyingset(scene);
}
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTAVAILABLE)) {
return builtin_keyingset_get_named(ANIM_KS_AVAILABLE_ID);
}
return builtin_keyingset_get_named(transformKSName);
}
KeyingSet *scene_get_active_keyingset(const Scene *scene)
{
/* If no scene, we've got no hope of finding the Keying Set. */
if (scene == nullptr) {
return nullptr;
}
/* Currently, there are several possibilities here:
* - 0: no active keying set
* - > 0: one of the user-defined Keying Sets, but indices start from 0 (hence the -1)
* - < 0: a builtin keying set
*/
if (scene->active_keyingset > 0) {
return static_cast<KeyingSet *>(BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
}
return static_cast<KeyingSet *>(
BLI_findlink(&builtin_keyingsets, (-scene->active_keyingset) - 1));
}
void relative_keyingset_add_source(Vector<PointerRNA> &sources,
ID *id,
StructRNA *srna,
void *data)
{
if (ELEM(nullptr, srna, data, id)) {
return;
}
sources.append(RNA_pointer_create_discrete(id, srna, data));
}
void relative_keyingset_add_source(Vector<PointerRNA> &sources, ID *id)
{
if (id == nullptr) {
return;
}
sources.append(RNA_id_pointer_create(id));
}
/* Special 'Overrides' Iterator for Relative KeyingSets ------ */
/* Iterator used for overriding the behavior of iterators defined for
* relative Keying Sets, with the main usage of this being operators
* requiring Auto Keyframing. Internal Use Only!
*/
static void RKS_ITER_overrides_list(KeyingSetInfo *keyingset_info,
bContext *C,
KeyingSet *keyingset,
Vector<PointerRNA> &sources)
{
for (PointerRNA ptr : sources) {
/* Run generate callback on this data. */
keyingset_info->generate(keyingset_info, C, keyingset, &ptr);
}
}
ModifyKeyReturn validate_keyingset(bContext *C, Vector<PointerRNA> *sources, KeyingSet *keyingset)
{
if (keyingset == nullptr) {
return ModifyKeyReturn::SUCCESS;
}
/* If relative Keying Sets, poll and build up the paths. */
if (keyingset->flag & KEYINGSET_ABSOLUTE) {
return ModifyKeyReturn::SUCCESS;
}
KeyingSetInfo *keyingset_info = keyingset_info_find_name(keyingset->typeinfo);
/* Clear all existing paths
* NOTE: BKE_keyingset_free_paths() frees all of the paths for the KeyingSet, but not the set
* itself.
*/
BKE_keyingset_free_paths(keyingset);
/* Get the associated 'type info' for this KeyingSet. */
if (keyingset_info == nullptr) {
return ModifyKeyReturn::MISSING_TYPEINFO;
}
/* TODO: check for missing callbacks! */
/* Check if it can be used in the current context. */
if (!keyingset_info->poll(keyingset_info, C)) {
/* Poll callback tells us that KeyingSet is useless in current context. */
/* FIXME: the poll callback needs to give us more info why. */
return ModifyKeyReturn::INVALID_CONTEXT;
}
/* If a list of data sources are provided, run a special iterator over them,
* otherwise, just continue per normal.
*/
if (sources != nullptr) {
RKS_ITER_overrides_list(keyingset_info, C, keyingset, *sources);
}
else {
keyingset_info->iter(keyingset_info, C, keyingset);
}
/* If we don't have any paths now, then this still qualifies as invalid context. */
/* FIXME: we need some error conditions (to be retrieved from the iterator why this failed!)
*/
if (keyingset->paths.is_empty()) {
return ModifyKeyReturn::INVALID_CONTEXT;
}
return ModifyKeyReturn::SUCCESS;
}
/* Determine which keying flags apply based on the override flags. */
static eInsertKeyFlags keyingset_apply_keying_flags(const eInsertKeyFlags base_flags,
const eInsertKeyFlags overrides,
const eInsertKeyFlags own_flags)
{
/* Pass through all flags by default (i.e. even not explicitly listed ones). */
eInsertKeyFlags result = base_flags;
/* The logic for whether a keying flag applies is as follows:
* - If the flag in question is set in "overrides", that means that the
* status of that flag in "own_flags" is used
* - If however the flag isn't set, then its value in "base_flags" is used
* instead (i.e. no override)
*/
#define APPLY_KEYINGFLAG_OVERRIDE(kflag) \
if (overrides & kflag) { \
result &= ~kflag; \
result |= (own_flags & kflag); \
}
/* Apply the flags one by one...
* (See rna_def_common_keying_flags() for the supported flags)
*/
APPLY_KEYINGFLAG_OVERRIDE(INSERTKEY_NEEDED)
APPLY_KEYINGFLAG_OVERRIDE(INSERTKEY_MATRIX)
#undef APPLY_KEYINGFLAG_OVERRIDE
return result;
}
static int insert_key_to_keying_set_path(bContext *C,
KS_Path *keyingset_path,
KeyingSet *keyingset,
const eInsertKeyFlags insert_key_flags,
const ModifyKeyMode mode,
const float frame)
{
if (!keyingset_path->rna_path) {
/* In case the path is incomplete/not filled in by the user. */
return 0;
}
/* Since keying settings can be defined on the paths too,
* apply the settings for this path first. */
const eInsertKeyFlags path_insert_key_flags = keyingset_apply_keying_flags(
insert_key_flags,
eInsertKeyFlags(keyingset_path->keyingoverride),
eInsertKeyFlags(keyingset_path->keyingflag));
const char *groupname = nullptr;
/* Get pointer to name of group to add channels to. */
if (keyingset_path->groupmode == KSP_GROUP_NONE) {
groupname = nullptr;
}
else if (keyingset_path->groupmode == KSP_GROUP_KSNAME) {
groupname = keyingset->name;
}
else {
groupname = keyingset_path->group;
}
/* Init - array_length should be greater than array_index so that
* normal non-array entries get keyframed correctly.
*/
int array_index = keyingset_path->array_index;
int array_length = array_index;
/* Get length of array if whole array option is enabled. */
if (keyingset_path->flag & KSP_FLAG_WHOLE_ARRAY) {
PointerRNA ptr;
PropertyRNA *prop;
PointerRNA id_ptr = RNA_id_pointer_create(keyingset_path->id);
if (RNA_path_resolve_property(&id_ptr, keyingset_path->rna_path, &ptr, &prop)) {
array_length = RNA_property_array_length(&ptr, prop);
/* Start from start of array, instead of the previously specified index - #48020 */
array_index = 0;
}
}
/* We should do at least one step. */
if (array_length == array_index) {
array_length++;
}
Main *bmain = CTX_data_main(C);
ReportList *reports = CTX_wm_reports(C);
Scene *scene = CTX_data_scene(C);
const eBezTriple_KeyframeType keytype = eBezTriple_KeyframeType(
scene->toolsettings->keyframe_type);
/* For each possible index, perform operation
* - Assume that array-length is greater than index. */
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
frame);
int keyed_channels = 0;
CombinedKeyingResult combined_result;
for (; array_index < array_length; array_index++) {
if (mode == ModifyKeyMode::INSERT) {
const std::optional<StringRefNull> group = groupname ? std::optional(groupname) :
std::nullopt;
const std::optional<int> index = array_index >= 0 ? std::optional(array_index) :
std::nullopt;
PointerRNA id_rna_pointer = RNA_id_pointer_create(keyingset_path->id);
CombinedKeyingResult result = insert_keyframes(bmain,
&id_rna_pointer,
group,
{{keyingset_path->rna_path, {}, index}},
std::nullopt,
anim_eval_context,
keytype,
path_insert_key_flags);
keyed_channels += result.get_count(SingleKeyingResult::SUCCESS);
combined_result.merge(result);
}
else if (mode == ModifyKeyMode::DELETE_KEY) {
RNAPath rna_path = {keyingset_path->rna_path, std::nullopt, array_index};
if (array_index < 0) {
rna_path.index = std::nullopt;
}
keyed_channels += delete_keyframe(bmain, reports, keyingset_path->id, rna_path, frame);
}
}
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
combined_result.generate_reports(reports);
}
switch (GS(keyingset_path->id->name)) {
case ID_OB: /* Object (or Object-Related) Keyframes */
{
Object *ob = reinterpret_cast<Object *>(keyingset_path->id);
/* XXX: only object transforms? */
DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
break;
}
default:
DEG_id_tag_update(keyingset_path->id, ID_RECALC_ANIMATION_NO_FLUSH);
break;
}
WM_main_add_notifier(NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr);
return keyed_channels;
}
int apply_keyingset(bContext *C,
Vector<PointerRNA> *sources,
KeyingSet *keyingset,
const ModifyKeyMode mode,
const float cfra)
{
if (keyingset == nullptr) {
return 0;
}
Scene *scene = CTX_data_scene(C);
const eInsertKeyFlags base_kflags = get_keyframing_flags(scene);
eInsertKeyFlags kflag = INSERTKEY_NOFLAGS;
if (mode == ModifyKeyMode::INSERT) {
/* Use context settings as base. */
kflag = keyingset_apply_keying_flags(base_kflags,
eInsertKeyFlags(keyingset->keyingoverride),
eInsertKeyFlags(keyingset->keyingflag));
}
else if (mode == ModifyKeyMode::DELETE_KEY) {
kflag = INSERTKEY_NOFLAGS;
}
/* If relative Keying Sets, poll and build up the paths. */
{
const ModifyKeyReturn error = validate_keyingset(C, sources, keyingset);
if (error != ModifyKeyReturn::SUCCESS) {
BLI_assert(int(error) < 0);
return int(error);
}
}
ReportList *reports = CTX_wm_reports(C);
int keyed_channels = 0;
/* Apply the paths as specified in the KeyingSet now. */
for (KS_Path &keyingset_path : keyingset->paths) {
/* Skip path if no ID pointer is specified. */
if (keyingset_path.id == nullptr) {
BKE_reportf(reports,
RPT_WARNING,
"Skipping path in keying set, as it has no ID (KS = '%s', path = '%s[%d]')",
keyingset->name,
keyingset_path.rna_path,
keyingset_path.array_index);
continue;
}
keyed_channels += insert_key_to_keying_set_path(
C, &keyingset_path, keyingset, kflag, mode, cfra);
}
/* Return the number of channels successfully affected. */
BLI_assert(keyed_channels >= 0);
return keyed_channels;
}
} // namespace animrig
} // namespace blender

View File

@@ -0,0 +1,251 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "BLI_bit_vector.hh"
#include "BLI_dynstr.h"
#include "BKE_animsys.h"
#include "BKE_fcurve.hh"
#include "RNA_access.hh"
#include "RNA_types.hh"
#include "BLT_translation.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_nla.hh"
namespace blender::animrig::nla {
bool assign_action(NlaStrip &strip, Action &action, ID &animated_id)
{
if (!generic_assign_action(
animated_id, &action, strip.act, strip.action_slot_handle, strip.last_slot_identifier))
{
return false;
}
/* For the NLA, the auto slot selection gets one more fallback option (compared to the generic
* code). This is to support the following scenario:
*
* - Python script creates an Action, and adds some F-Curves via the legacy API.
* - This creates a slot 'XXSlot'.
* - The script creates multiple NLA strips for that Action.
* - The desired result is that these strips get the same Slot assigned as well.
*
* The generic code doesn't work for this. The first strip assignment would see the slot
* `XXSlot`, and because it has never been used, just use it. This would change its name to, for
* example, `OBSlot`. The second strip assignment would not see a 'virgin' slot, and thus not
* auto-select `OBSlot`. This behavior makes sense when assigning Actions in the Action editor
* (it shouldn't automatically pick the first slot of matching ID type), but for the NLA I
* (Sybren) feel that it could be a bit more 'enthusiastic' in auto-picking a slot.
*/
if (strip.action_slot_handle == Slot::unassigned && action.slots().size() == 1) {
Slot *first_slot = action.slot(0);
if (first_slot->is_suitable_for(animated_id)) {
const ActionSlotAssignmentResult result = assign_action_slot(strip, first_slot, animated_id);
BLI_assert_msg(result == ActionSlotAssignmentResult::OK,
"Assigning a slot that we know is suitable should work");
UNUSED_VARS_NDEBUG(result);
}
}
/* Regardless of slot auto-selection, the Action assignment worked just fine. */
return true;
}
void unassign_action(NlaStrip &strip, ID &animated_id)
{
const bool ok = generic_assign_action(
animated_id, nullptr, strip.act, strip.action_slot_handle, strip.last_slot_identifier);
BLI_assert_msg(ok, "Un-assigning an Action from an NLA strip should always work.");
UNUSED_VARS_NDEBUG(ok);
}
ActionSlotAssignmentResult assign_action_slot(NlaStrip &strip,
Slot *slot_to_assign,
ID &animated_id)
{
BLI_assert(strip.act);
return generic_assign_action_slot(slot_to_assign,
animated_id,
strip.act,
strip.action_slot_handle,
strip.last_slot_identifier);
}
ActionSlotAssignmentResult assign_action_slot_handle(NlaStrip &strip,
const slot_handle_t slot_handle,
ID &animated_id)
{
BLI_assert(strip.act);
Action &action = strip.act->wrap();
Slot *slot_to_assign = action.slot_for_handle(slot_handle);
return assign_action_slot(strip, slot_to_assign, animated_id);
}
/* Check indices that were intended to be remapped and report any failed remaps. */
static void get_keyframe_values_create_reports(ReportList *reports,
const PointerRNA &ptr,
const PropertyRNA *prop,
const int index,
const int count,
const bool force_all,
const BitSpan successful_remaps)
{
DynStr *ds_failed_indices = BLI_dynstr_new();
int total_failed = 0;
for (int i = 0; i < count; i++) {
const bool cur_index_evaluated = ELEM(index, i, -1) || force_all;
if (!cur_index_evaluated) {
/* `values[i]` was never intended to be remapped. */
continue;
}
if (successful_remaps[i]) {
/* `values[i]` successfully remapped. */
continue;
}
total_failed++;
/* Report that `values[i]` were intended to be remapped but failed remapping process. */
BLI_dynstr_appendf(ds_failed_indices, "%d, ", i);
}
if (total_failed == 0) {
BLI_dynstr_free(ds_failed_indices);
return;
}
char *str_failed_indices = BLI_dynstr_get_cstring(ds_failed_indices);
BLI_dynstr_free(ds_failed_indices);
BKE_reportf(reports,
RPT_WARNING,
"Could not insert %i keyframe(s) due to zero NLA influence, base value, or value "
"remapping failed: %s.%s for indices [%s]",
total_failed,
ptr.owner_id->name,
RNA_property_ui_name(prop),
str_failed_indices);
MEM_delete(str_failed_indices);
}
static BitVector<> nla_map_keyframe_values_and_generate_reports(
const MutableSpan<float> values,
const int index,
PointerRNA &ptr,
PropertyRNA &prop,
NlaKeyframingContext *nla_context,
const AnimationEvalContext *anim_eval_context,
ReportList *reports,
bool *force_all)
{
BitVector<> successful_remaps(values.size(), false);
BKE_animsys_nla_remap_keyframe_values(
nla_context, &ptr, &prop, values, index, anim_eval_context, force_all, successful_remaps);
get_keyframe_values_create_reports(
reports, ptr, &prop, index, values.size(), false, successful_remaps);
return successful_remaps;
}
bool insert_keyframe_direct(ReportList *reports,
PointerRNA ptr,
PropertyRNA *prop,
FCurve *fcu,
const AnimationEvalContext *anim_eval_context,
eBezTriple_KeyframeType keytype,
NlaKeyframingContext *nla_context,
eInsertKeyFlags flag)
{
if (fcu == nullptr) {
BKE_report(reports, RPT_ERROR, "No F-Curve to add keyframes to");
return false;
}
if (!BKE_fcurve_is_keyframable(*fcu)) {
BKE_report(reports, RPT_ERROR, "FCurve is not keyable. Cannot insert keyframes");
return false;
}
if ((ptr.owner_id == nullptr) && (ptr.data == nullptr)) {
BKE_report(
reports, RPT_ERROR, "No RNA pointer available to retrieve values for keyframing from");
return false;
}
if (prop == nullptr) {
PointerRNA tmp_ptr;
if (RNA_path_resolve_property(&ptr, fcu->rna_path, &tmp_ptr, &prop) == false) {
const char *idname = (ptr.owner_id) ? ptr.owner_id->name : RPT_("<No ID pointer>");
BKE_reportf(reports,
RPT_ERROR,
"Could not insert keyframe, as RNA path is invalid for the given ID (ID = %s, "
"path = %s)",
idname,
fcu->rna_path);
return false;
}
/* Property found, so overwrite 'ptr' to make later code easier. */
ptr = tmp_ptr;
}
/* Update F-Curve flags to ensure proper behavior for property type. */
update_autoflags_fcurve_direct(fcu, RNA_property_type(prop));
const int index = fcu->array_index;
const bool visual_keyframing = flag & INSERTKEY_MATRIX;
Vector<float> values = get_property_values(&ptr, prop, visual_keyframing);
BitVector<> successful_remaps = nla_map_keyframe_values_and_generate_reports(
values.as_mutable_span(),
index,
ptr,
*prop,
nla_context,
anim_eval_context,
reports,
nullptr);
float current_value = 0.0f;
if (index >= 0 && index < values.size()) {
current_value = values[index];
}
/* This happens if NLA rejects this insertion. */
if (!successful_remaps[index]) {
return false;
}
KeyframeSettings settings = get_keyframe_settings((flag & INSERTKEY_NO_USERPREF) == 0);
settings.keyframe_type = keytype;
const SingleKeyingResult result = insert_vert_fcurve(
fcu, {anim_eval_context->eval_time, current_value}, settings, flag);
if (result != SingleKeyingResult::SUCCESS) {
BKE_reportf(reports,
RPT_ERROR,
"Failed to insert keys on F-Curve with path '%s[%d]', ensure that it is not "
"locked or sampled, and try removing F-Modifiers",
fcu->rna_path,
fcu->array_index);
}
return result == SingleKeyingResult::SUCCESS;
}
} // namespace blender::animrig::nla

View File

@@ -0,0 +1,160 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_nla.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_nla.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "BLI_listbase.h"
#include "testing/testing.h"
namespace blender::animrig::nla::tests {
class NLASlottedActionTest : public bke::BlenderGTestBase {
public:
Main *bmain;
Action *action;
Object *cube;
void SetUp() override
{
bmain = BKE_main_new();
action = BKE_id_new<Action>(bmain, "ACÄnimåtië");
action->id.us = 0; /* Nothing references this yet. */
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
cube->id.us = 0; /* Nothing references this yet. */
}
void TearDown() override
{
BKE_main_free(bmain);
}
};
TEST_F(NLASlottedActionTest, assign_slot_to_nla_strip)
{
ASSERT_EQ(action->id.us, 0);
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
NlaTrack *track = BKE_nlatrack_new_tail(&adt->nla_tracks, false);
/* Create a strip. This automatically assigns the Action, but for now with the old flow. */
NlaStrip *strip = BKE_nlastrip_new(action, cube->id);
BKE_nlatrack_add_strip(track, strip, false);
EXPECT_EQ(strip->action_slot_handle, Slot::unassigned);
EXPECT_STREQ(strip->last_slot_identifier, "");
/* Unassign the Action that was automatically assigned via BKE_nlastrip_new(). */
nla::unassign_action(*strip, cube->id);
EXPECT_EQ(strip->act, nullptr);
EXPECT_EQ(action->id.us, 0);
/* Assign an Action with a never-assigned slot. This should be picked automatically. */
Slot &virgin_slot = action->slot_add();
/* Assign the Action. */
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
EXPECT_EQ(strip->action_slot_handle, virgin_slot.handle);
EXPECT_STREQ(strip->last_slot_identifier, virgin_slot.identifier);
EXPECT_EQ(action->id.us, 1);
EXPECT_EQ(strip->act, action);
EXPECT_EQ(virgin_slot.idtype, GS(cube->id.name));
/* Unassign the Action. */
nla::unassign_action(*strip, cube->id);
EXPECT_EQ(strip->act, nullptr);
EXPECT_EQ(action->id.us, 0);
/* Create a slot for this ID, and make the NLA strip forget what slot it was assigned to before.
* Assigning the Action should now auto-pick the slot with the ID name. */
Slot &slot = action->slot_add_for_id(cube->id);
strip->last_slot_identifier[0] = '\0';
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
EXPECT_EQ(strip->action_slot_handle, slot.handle);
EXPECT_STREQ(strip->last_slot_identifier, slot.identifier);
EXPECT_EQ(action->id.us, 1);
EXPECT_EQ(strip->act, action);
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
/* Unassign the slot, but keep the Action assigned. */
EXPECT_EQ(nla::assign_action_slot(*strip, nullptr, cube->id), ActionSlotAssignmentResult::OK);
EXPECT_EQ(strip->action_slot_handle, Slot::unassigned);
EXPECT_STREQ(strip->last_slot_identifier, slot.identifier);
EXPECT_EQ(action->id.us, 1);
EXPECT_EQ(strip->act, action);
EXPECT_FALSE(slot.runtime_users().contains(&cube->id));
/* Unassign the Action, then reassign it. It should pick the same slot again. */
nla::unassign_action(*strip, cube->id);
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
EXPECT_EQ(strip->action_slot_handle, slot.handle);
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
}
TEST_F(NLASlottedActionTest, assign_slot_to_multiple_strips)
{
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
NlaTrack *track = BKE_nlatrack_new_tail(&adt->nla_tracks, false);
/* Create two strips. This automatically assigns the Action, but for now with
* the old flow (so no slots). */
NlaStrip *strip1 = BKE_nlastrip_new(action, cube->id);
strip1->start = 1;
strip1->end = 4;
NlaStrip *strip2 = BKE_nlastrip_new(action, cube->id);
strip1->start = 47;
strip1->end = 327;
ASSERT_TRUE(BKE_nlatrack_add_strip(track, strip1, false));
ASSERT_TRUE(BKE_nlatrack_add_strip(track, strip2, false));
ASSERT_EQ(1, adt->nla_tracks.count());
ASSERT_EQ(2, track->strips.count());
nla::unassign_action(*strip1, cube->id);
nla::unassign_action(*strip2, cube->id);
/* Create a virgin slot, it should be auto-picked. */
Slot &slot = action->slot_add();
EXPECT_TRUE(nla::assign_action(*strip1, *action, cube->id));
EXPECT_EQ(strip1->action_slot_handle, slot.handle);
EXPECT_STREQ(strip1->last_slot_identifier, slot.identifier);
EXPECT_EQ(slot.idtype, ID_OB);
/* Assign another slot 'manually'. */
Slot &other_slot = action->slot_add();
EXPECT_EQ(nla::assign_action_slot(*strip1, &other_slot, cube->id),
ActionSlotAssignmentResult::OK);
EXPECT_EQ(strip1->action_slot_handle, other_slot.handle);
/* Assign the Action + slot to the second strip. */
EXPECT_TRUE(nla::assign_action(*strip2, *action, cube->id));
EXPECT_EQ(nla::assign_action_slot(*strip2, &slot, cube->id), ActionSlotAssignmentResult::OK);
/* The cube should be registered as user of the slot. */
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
nla::unassign_action(*strip1, cube->id);
/* The cube should still be registered as user of the slot, as there is a 2nd
* strip that references it. */
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
/* Remove the last use of this slot. */
nla::unassign_action(*strip2, cube->id);
EXPECT_FALSE(slot.runtime_users().contains(&cube->id));
}
} // namespace blender::animrig::nla::tests

View File

@@ -0,0 +1,170 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_pose.hh"
#include "BKE_action.hh"
#include "BKE_animsys.h"
#include "BKE_armature.hh"
#include "BLI_listbase.h"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "ANIM_action.hh"
namespace blender::animrig {
namespace {
using ActionApplier =
FunctionRef<void(PointerRNA *, bAction *, slot_handle_t, const AnimationEvalContext *)>;
void pose_apply_restore_fcurves(const Span<FCurve *> fcurves)
{
for (FCurve *fcu : fcurves) {
fcu->flag &= ~FCURVE_DISABLED;
}
}
/* Returns a vector of all FCurves on which the fcurve flag was modified. */
Vector<FCurve *> pose_apply_disable_fcurves_for_unselected_bones(
bAction *action, const slot_handle_t slot_handle, const bke::BoneNameSet &selected_bone_names)
{
Vector<FCurve *> modified_fcurves;
auto disable_unselected_fcurve = [&](FCurve *fcu, const char *bone_name) {
const bool is_bone_selected = selected_bone_names.contains(bone_name);
if (!is_bone_selected) {
if (!(fcu->flag & FCURVE_DISABLED)) {
/* FCurve is not yet disabled, we need to reset that later. */
modified_fcurves.append(fcu);
}
fcu->flag |= FCURVE_DISABLED;
}
};
bke::BKE_action_find_fcurves_with_bones(action, slot_handle, disable_unselected_fcurve);
return modified_fcurves;
}
void pose_apply(Object *ob,
bAction *action,
const slot_handle_t slot_handle,
const AnimationEvalContext *anim_eval_context,
ActionApplier applier)
{
bPose *pose = ob->pose;
if (pose == nullptr) {
return;
}
if (action->wrap().slot_array_num == 0) {
return;
}
const bke::BoneNameSet selected_bone_names = bke::BKE_pose_channel_find_selected_names(ob);
/* Mute all FCurves that are not associated with selected bones. This separates the concept of
* bone selection from the FCurve evaluation code. */
Vector<FCurve *> modified_fcurves = pose_apply_disable_fcurves_for_unselected_bones(
action, slot_handle, selected_bone_names);
/* Apply the Action. */
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
applier(&pose_owner_ptr, action, slot_handle, anim_eval_context);
pose_apply_restore_fcurves(modified_fcurves);
}
} // namespace
void pose_apply_action_all_bones(Object *ob,
bAction *action,
const int32_t slot_handle,
const AnimationEvalContext *anim_eval_context)
{
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
animsys_evaluate_action(&pose_owner_ptr, action, slot_handle, anim_eval_context, false);
}
void pose_apply_action_blend(Object *ob,
bAction *action,
const int32_t slot_handle,
const AnimationEvalContext *anim_eval_context,
const float blend_factor)
{
auto evaluate_and_blend = [blend_factor](PointerRNA *ptr,
bAction *act,
const int32_t slot_handle,
const AnimationEvalContext *anim_eval_context) {
animsys_blend_in_action(ptr, act, slot_handle, anim_eval_context, blend_factor);
};
pose_apply(ob, action, slot_handle, anim_eval_context, evaluate_and_blend);
}
void pose_apply_action_blend_all_bones(Object *ob,
bAction *action,
slot_handle_t slot_handle,
const AnimationEvalContext *anim_eval_context,
const float blend_factor)
{
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
animsys_blend_in_action(&pose_owner_ptr, action, slot_handle, anim_eval_context, blend_factor);
}
bool any_bone_selected(const Span<const Object *> objects)
{
for (const Object *obj : objects) {
if (!obj->pose) {
continue;
}
for (bPoseChannel &pose_bone : obj->pose->chanbase) {
if (pose_bone.flag & POSE_SELECTED) {
return true;
}
}
}
return false;
}
void pose_apply_action(const Span<Object *> objects,
Action &pose_action,
const AnimationEvalContext *anim_eval_context,
const float blend_factor)
{
if (any_bone_selected(objects)) {
for (Object *object : objects) {
Slot &slot = get_best_pose_slot_for_id(object->id, pose_action);
pose_apply_action_blend(object, &pose_action, slot.handle, anim_eval_context, blend_factor);
}
}
else {
/* In the case of nothing selected, act as if all is selected. This is a convenience feature
* for the artists so they don't have to be specific in their selection all the time. */
for (Object *object : objects) {
Slot &slot = get_best_pose_slot_for_id(object->id, pose_action);
pose_apply_action_blend_all_bones(
object, &pose_action, slot.handle, anim_eval_context, blend_factor);
}
}
}
Slot &get_best_pose_slot_for_id(const ID &id, Action &pose_data)
{
BLI_assert_msg(pose_data.slot_array_num > 0,
"Actions without slots have no data. This should have been caught earlier.");
Slot *slot = generic_slot_for_autoassign(id, pose_data, "");
if (slot == nullptr) {
slot = pose_data.slot(0);
}
return *slot;
}
} // namespace blender::animrig

View File

@@ -0,0 +1,562 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_pose.hh"
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_string.h"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_armature.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DEG_depsgraph.hh"
#include "DNA_object_types.h"
#include "ANIM_action.hh"
#include "ANIM_pose.hh"
#include "testing/testing.h"
namespace blender {
constexpr char msg_unexpected_modification[] =
"Properties not stored in the pose are expected to not be modified.";
namespace animrig::tests {
class PoseTest : public bke::BlenderGTestBase {
public:
Main *bmain;
Action *pose_action;
Object *obj_empty;
Object *obj_armature_a;
Object *obj_armature_b;
StripKeyframeData *keyframe_data;
const animrig::KeyframeSettings key_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO, BEZT_IPO_BEZ};
void SetUp() override
{
bmain = BKE_main_new();
pose_action = BKE_id_new<Action>(bmain, "pose_data");
Layer &layer = pose_action->layer_add("first_layer");
Strip &strip = layer.strip_add(*pose_action, Strip::Type::Keyframe);
keyframe_data = &strip.data<StripKeyframeData>(*pose_action);
obj_empty = BKE_object_add_only_object(bmain, OB_EMPTY, "obj_empty");
obj_armature_a = BKE_object_add_only_object(bmain, OB_ARMATURE, "obj_armature_a");
obj_armature_b = BKE_object_add_only_object(bmain, OB_ARMATURE, "obj_armature_b");
bArmature *armature = BKE_armature_add(bmain, "ArmatureA");
obj_armature_a->data = id_cast<ID *>(armature);
Bone *bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "BoneA");
BLI_addtail(&armature->bonebase, bone);
bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "BoneB");
BLI_addtail(&armature->bonebase, bone);
BKE_pose_ensure(bmain, obj_armature_a, armature, false);
armature = BKE_armature_add(bmain, "ArmatureB");
obj_armature_b->data = id_cast<ID *>(armature);
bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "BoneA");
BLI_addtail(&armature->bonebase, bone);
bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "BoneB");
BLI_addtail(&armature->bonebase, bone);
BKE_pose_ensure(bmain, obj_armature_b, armature, false);
}
void TearDown() override
{
BKE_main_free(bmain);
}
};
TEST_F(PoseTest, get_best_slot)
{
Slot &first_slot = pose_action->slot_add();
Slot &second_slot = pose_action->slot_add_for_id(obj_empty->id);
EXPECT_EQ(&get_best_pose_slot_for_id(obj_empty->id, *pose_action), &second_slot);
EXPECT_EQ(&get_best_pose_slot_for_id(obj_armature_a->id, *pose_action), &first_slot);
}
TEST_F(PoseTest, apply_action_object)
{
/* Since pose bones live on the object, the code is already set up to handle objects
* transforms, even though the name suggests it only applies to bones. */
Slot &first_slot = pose_action->slot_add();
EXPECT_EQ(obj_empty->loc[0], 0.0f);
keyframe_data->keyframe_insert(bmain, first_slot, {"location", 0}, {1, 10}, key_settings);
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action_all_bones(obj_empty, pose_action, first_slot.handle, &eval_context);
EXPECT_EQ(obj_empty->loc[0], 10.0f);
}
TEST_F(PoseTest, apply_action_all_bones_single_slot)
{
Slot &first_slot = pose_action->slot_add();
keyframe_data->keyframe_insert(
bmain, first_slot, {"pose.bones[\"BoneA\"].location", 0}, {1, 10}, key_settings);
keyframe_data->keyframe_insert(
bmain, first_slot, {"pose.bones[\"BoneB\"].location", 1}, {1, 5}, key_settings);
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
bPoseChannel *bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
bone_a->loc[1] = 1.0;
bone_a->loc[2] = 2.0;
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action_all_bones(
obj_armature_a, pose_action, first_slot.handle, &eval_context);
EXPECT_EQ(bone_a->loc[0], 10.0);
EXPECT_EQ(bone_b->loc[1], 5.0);
EXPECT_EQ(bone_a->loc[1], 1.0) << msg_unexpected_modification;
EXPECT_EQ(bone_a->loc[2], 2.0);
}
TEST_F(PoseTest, apply_action_all_bones_multiple_slots)
{
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
Slot &slot_b = pose_action->slot_add_for_id(obj_armature_b->id);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_b, {"pose.bones[\"BoneA\"].location", 1}, {1, 10}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_b, {"pose.bones[\"BoneB\"].location", 1}, {1, 10}, key_settings);
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action_all_bones(obj_armature_a, pose_action, slot_a.handle, &eval_context);
EXPECT_EQ(arm_a_bone_a->loc[0], 5.0);
EXPECT_EQ(arm_a_bone_a->loc[1], 0.0) << msg_unexpected_modification;
EXPECT_EQ(arm_a_bone_a->loc[2], 0.0) << msg_unexpected_modification;
EXPECT_EQ(arm_a_bone_b->loc[0], 5.0);
EXPECT_EQ(arm_b_bone_a->loc[1], 0.0) << "Other armature should not be affected yet.";
animrig::pose_apply_action_all_bones(obj_armature_b, pose_action, slot_b.handle, &eval_context);
EXPECT_EQ(arm_b_bone_b->loc[0], 0.0) << msg_unexpected_modification;
EXPECT_EQ(arm_b_bone_b->loc[1], 10.0);
EXPECT_EQ(arm_b_bone_b->loc[2], 0.0) << msg_unexpected_modification;
EXPECT_EQ(arm_a_bone_a->loc[0], 5.0) << "Other armature should not be affected.";
/* Any slot can be applied, even if it hasn't been added for the ID. */
animrig::pose_apply_action_all_bones(obj_armature_a, pose_action, slot_b.handle, &eval_context);
EXPECT_EQ(arm_b_bone_b->loc[1], arm_b_bone_a->loc[1])
<< "Applying the same pose should result in the same values.";
}
TEST_F(PoseTest, apply_action_blend_single_slot)
{
Slot &first_slot = pose_action->slot_add();
keyframe_data->keyframe_insert(
bmain, first_slot, {"pose.bones[\"BoneA\"].location", 0}, {1, 10}, key_settings);
keyframe_data->keyframe_insert(
bmain, first_slot, {"pose.bones[\"BoneB\"].location", 1}, {1, 5}, key_settings);
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
bPoseChannel *bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
bone_a->loc[0] = 0.0;
bone_b->loc[1] = 0.0;
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action_blend_all_bones(
obj_armature_a, pose_action, first_slot.handle, &eval_context, 1.0);
EXPECT_NEAR(bone_a->loc[0], 10.0, 0.001);
EXPECT_NEAR(bone_b->loc[1], 5.0, 0.001);
bone_a->loc[0] = 0.0;
bone_b->loc[1] = 0.0;
animrig::pose_apply_action_blend_all_bones(
obj_armature_a, pose_action, first_slot.handle, &eval_context, 0.5);
EXPECT_NEAR(bone_a->loc[0], 5.0, 0.001);
EXPECT_NEAR(bone_b->loc[1], 2.5, 0.001);
bone_a->loc[0] = 0.0;
bone_b->loc[1] = 0.0;
bone_a->flag |= POSE_SELECTED;
bone_b->flag &= ~POSE_SELECTED;
/* This should only affect the selected bone. */
animrig::pose_apply_action_blend(
obj_armature_a, pose_action, first_slot.handle, &eval_context, 0.5);
EXPECT_NEAR(bone_a->loc[0], 5.0, 0.001);
EXPECT_NEAR(bone_b->loc[1], 0.0, 0.001);
}
TEST_F(PoseTest, apply_action_multiple_objects)
{
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
Slot &slot_b = pose_action->slot_add_for_id(obj_armature_b->id);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_b, {"pose.bones[\"BoneA\"].location", 1}, {1, 10}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_b, {"pose.bones[\"BoneB\"].location", 1}, {1, 10}, key_settings);
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
Vector<bPoseChannel *> all_bones = {arm_a_bone_a, arm_a_bone_b, arm_b_bone_a, arm_b_bone_b};
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->flag &= ~POSE_SELECTED;
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
/* No bones are selected, this should affect all bones. */
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[1], 10, 0.001);
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
arm_a_bone_a->flag |= POSE_SELECTED;
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
/* Only the one selected bone should be affected. */
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[1], 0, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
arm_a_bone_a->flag |= POSE_SELECTED;
arm_b_bone_a->flag |= POSE_SELECTED;
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
/* Only the two selected bones from different armatures should be affected. */
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 0.5);
/* Blending half way. */
EXPECT_NEAR(arm_a_bone_a->loc[0], 2.5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[1], 5, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
arm_a_bone_a->flag |= POSE_SELECTED;
arm_a_bone_b->flag |= POSE_SELECTED;
arm_b_bone_a->flag |= POSE_SELECTED;
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
}
TEST_F(PoseTest, apply_action_multiple_objects_single_slot)
{
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
Vector<bPoseChannel *> all_bones = {arm_a_bone_a, arm_a_bone_b, arm_b_bone_a, arm_b_bone_b};
for (bPoseChannel *pose_bone : all_bones) {
pose_bone->flag &= ~POSE_SELECTED;
pose_bone->loc[0] = 0.0;
pose_bone->loc[1] = 0.0;
}
AnimationEvalContext eval_context = {nullptr, 1.0f};
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
/* No bones are selected, this should affect all bones. Armature B has no slot, it should fall
* back to slot 0. */
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
EXPECT_NEAR(arm_b_bone_a->loc[0], 5, 0.001);
EXPECT_NEAR(arm_b_bone_b->loc[0], 5, 0.001);
}
static void reset_pose_bone_rotations(bPoseChannel &pose_bone)
{
pose_bone.eul[0] = 0;
pose_bone.eul[1] = 0;
pose_bone.eul[2] = 0;
pose_bone.quat[0] = 1;
pose_bone.quat[1] = 0;
pose_bone.quat[2] = 0;
pose_bone.quat[3] = 0;
pose_bone.rotAngle = 0;
pose_bone.rotAxis[0] = 0;
pose_bone.rotAxis[1] = 0;
pose_bone.rotAxis[2] = 0;
}
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_euler)
{
/* When the pose has a different rotation mode than the data it is being applied to, the system
* should convert the rotation. */
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 0}, {1, 3.14}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 1}, {1, 1}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 2}, {1, 0}, key_settings);
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
AnimationEvalContext eval_context = {nullptr, 1.0f};
/* First check that applying works if the rotation mode matches. */
bone_a->rotmode = ROT_MODE_XYZ;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
EXPECT_NEAR(bone_a->eul[0], 3.14, 0.001);
EXPECT_NEAR(bone_a->eul[1], 1, 0.001);
EXPECT_NEAR(bone_a->eul[2], 0, 0.001);
BKE_pchan_calc_mat(pchanbone_a);
float expected_matrix[4][4];
copy_m4_m4(expected_matrix, bone_a->chan_mat);
/* Check that other rotation modes work the same as applying euler directly. */
bone_a->rotmode = ROT_MODE_QUAT;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
bone_a->rotmode = ROT_MODE_AXISANGLE;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
/* Not doing blend testing here since the rotation matrix will not align. Component wise
* interpolation of euler angles and matrix interpolation is expected to yield different
* results. */
}
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_quaternion)
{
/* When the pose has a different rotation mode than the data it is being applied to, the system
* should convert the rotation. */
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
float quaternion[4] = {0.877, 0.11, -0.483, -0.164};
/* We have to have a normalized quaternion otherwise the resulting matrix will be off between
* different rotation modes. */
normalize_qt(quaternion);
keyframe_data->keyframe_insert(bmain,
slot_a,
{"pose.bones[\"BoneA\"].rotation_quaternion", 0},
{1, quaternion[0]},
key_settings);
keyframe_data->keyframe_insert(bmain,
slot_a,
{"pose.bones[\"BoneA\"].rotation_quaternion", 1},
{1, quaternion[1]},
key_settings);
keyframe_data->keyframe_insert(bmain,
slot_a,
{"pose.bones[\"BoneA\"].rotation_quaternion", 2},
{1, quaternion[2]},
key_settings);
keyframe_data->keyframe_insert(bmain,
slot_a,
{"pose.bones[\"BoneA\"].rotation_quaternion", 3},
{1, quaternion[3]},
key_settings);
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
AnimationEvalContext eval_context = {nullptr, 1.0f};
/* First check that applying works if the rotation mode matches. */
bone_a->rotmode = ROT_MODE_QUAT;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
EXPECT_NEAR(bone_a->quat[0], quaternion[0], 0.001);
EXPECT_NEAR(bone_a->quat[1], quaternion[1], 0.001);
EXPECT_NEAR(bone_a->quat[2], quaternion[2], 0.001);
EXPECT_NEAR(bone_a->quat[3], quaternion[3], 0.001);
BKE_pchan_calc_mat(pchanbone_a);
float expected_matrix[4][4];
copy_m4_m4(expected_matrix, bone_a->chan_mat);
/* Check that other rotation modes work the same as applying quaternion directly. */
bone_a->rotmode = ROT_MODE_XYZ;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
bone_a->rotmode = ROT_MODE_AXISANGLE;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
reset_pose_bone_rotations(*bone_a);
/* Also test with blend factor other than 1. */
bone_a->rotmode = ROT_MODE_QUAT;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
copy_m4_m4(expected_matrix, bone_a->chan_mat);
bone_a->rotmode = ROT_MODE_AXISANGLE;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
bone_a->rotmode = ROT_MODE_XYZ;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
}
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_axisangle)
{
/* When the pose has a different rotation mode than the data it is being applied to, the system
* should convert the rotation. */
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 0}, {1, 0.66}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 1}, {1, -0.3}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 2}, {1, 0.86}, key_settings);
keyframe_data->keyframe_insert(
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 3}, {1, -0.42}, key_settings);
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
AnimationEvalContext eval_context = {nullptr, 1.0f};
/* First check that applying works if the rotation mode matches. */
bone_a->rotmode = ROT_MODE_AXISANGLE;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
EXPECT_NEAR(bone_a->rotAngle, 0.66, 0.001);
EXPECT_NEAR(bone_a->rotAxis[0], -0.3, 0.001);
EXPECT_NEAR(bone_a->rotAxis[1], 0.86, 0.001);
EXPECT_NEAR(bone_a->rotAxis[2], -0.42, 0.001);
BKE_pchan_calc_mat(pchanbone_a);
float expected_matrix[4][4];
copy_m4_m4(expected_matrix, bone_a->chan_mat);
/* Check that other rotation modes work the same as applying quaternion directly. */
bone_a->rotmode = ROT_MODE_XYZ;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
bone_a->rotmode = ROT_MODE_QUAT;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
reset_pose_bone_rotations(*bone_a);
/* Also test with blend factor other than 1. */
bone_a->rotmode = ROT_MODE_AXISANGLE;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
copy_m4_m4(expected_matrix, bone_a->chan_mat);
bone_a->rotmode = ROT_MODE_QUAT;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
bone_a->rotmode = ROT_MODE_XYZ;
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
BKE_pchan_calc_mat(pchanbone_a);
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
}
} // namespace animrig::tests
} // namespace blender

View File

@@ -0,0 +1,406 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
/* This is versioning code, so it's allowed to touch on deprecated DNA fields. */
#define DNA_DEPRECATED_ALLOW
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_action_legacy.hh"
#include "ANIM_versioning.hh"
#include "DNA_action_types.h"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_nla.hh"
#include "BKE_node.hh"
#include "BKE_report.hh"
#include "BLI_listbase.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.hh"
#include "BLO_readfile.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
namespace blender::animrig::versioning {
bool action_is_layered(const bAction &dna_action)
{
/* NOTE: due to how forward-compatibility is handled when writing Actions to
* blend files, it is important that this function does NOT check
* `Action.idroot` as part of its determination of whether this is a layered
* action or not.
*
* See: `action_blend_write()` and `action_blend_read_data()`
*/
const animrig::Action &action = dna_action.wrap();
const bool has_layered_data = action.layer_array_num > 0 || action.slot_array_num > 0;
const bool has_animato_data = !(action.curves.is_empty() && action.groups.is_empty());
return has_layered_data || !has_animato_data;
}
void convert_legacy_animato_actions(Main &bmain)
{
for (bAction &dna_action : bmain.actions) {
animrig::Action &action = dna_action.wrap();
if (action_is_layered(action) && !action.is_empty()) {
/* This is just a safety net. Blender files that trigger this versioning code are not
* expected to have any layered/slotted Actions.
*
* Empty Actions, even though they are valid "layered" Actions, should still get through
* versioning, though, to ensure they have the default "Legacy Slot" and a zero idroot. */
continue;
}
convert_legacy_animato_action(action);
}
}
void convert_legacy_animato_action(bAction &dna_action)
{
Action &action = dna_action.wrap();
/* Check that this is a legacy action.
* Cannot use `!action_is_layered` because that would be false on empty actions. */
BLI_assert(action.layer_array_num == 0 && action.slot_array_num == 0);
/* Store this ahead of time, because adding the slot sets the action's idroot
* to 0. We also set the action's idroot to 0 manually, just to be defensive
* so we don't depend on esoteric behavior in `slot_add()`. */
const int16_t idtype = action.idroot;
action.idroot = 0;
/* Initialize the Action's last_slot_handle field to its default value, before
* we create a new slot. */
action.last_slot_handle = DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE;
Slot &slot = action.slot_add();
slot.idtype = idtype;
const std::string slot_identifier{slot.idtype_string() +
DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME)};
action.slot_identifier_define(slot, slot_identifier);
Layer &layer = action.layer_add(DATA_(legacy::DEFAULT_LEGACY_LAYER_NAME));
animrig::Strip &strip = layer.strip_add(action, animrig::Strip::Type::Keyframe);
Channelbag &bag = strip.data<StripKeyframeData>(action).channelbag_for_slot_ensure(slot);
const int fcu_count = action.curves.count();
const int group_count = action.groups.count();
bag.fcurve_array = MEM_new_array_zeroed<FCurve *>(fcu_count, "Action versioning - fcurves");
bag.fcurve_array_num = fcu_count;
bag.group_array = MEM_new_array_zeroed<bActionGroup *>(group_count,
"Action versioning - groups");
bag.group_array_num = group_count;
int fcurve_index = 0;
for (const auto [group_index, group] : action.groups.enumerate()) {
bag.group_array[group_index] = &group;
group.channelbag = &bag;
group.fcurve_range_start = fcurve_index;
for (FCurve &fcu : group.channels) {
if (fcu.grp != &group) {
break;
}
bag.fcurve_array[fcurve_index++] = &fcu;
}
group.fcurve_range_length = fcurve_index - group.fcurve_range_start;
}
for (FCurve &fcu : action.curves) {
/* Any fcurves with groups have already been added to the fcurve array. */
if (fcu.grp) {
continue;
}
bag.fcurve_array[fcurve_index++] = &fcu;
}
BLI_assert(fcurve_index == fcu_count);
action.curves = {nullptr, nullptr};
action.groups = {nullptr, nullptr};
}
void tag_action_user_for_slotted_actions_conversion(ID &animated_id)
{
animated_id.runtime->readfile_data->tags.action_assignment_needs_slot = true;
}
void tag_action_users_for_slotted_actions_conversion(Main &bmain)
{
/* This function is only called when the blend-file is old enough to NOT use
* slotted Actions, so we can safely tag anything that uses an Action. */
auto flag_adt = [](ID &animated_id,
bAction *& /*action_ptr_ref*/,
slot_handle_t & /*slot_handle_ref*/,
char * /*last_slot_identifier*/) -> bool {
tag_action_user_for_slotted_actions_conversion(animated_id);
/* Once tagged, the foreach loop can stop, because more tagging of the same
* ID doesn't do anything. */
return false;
};
ID *id;
FOREACH_MAIN_ID_BEGIN (&bmain, id) {
foreach_action_slot_use_with_references(*id, flag_adt);
/* Process embedded IDs, as these are not listed in bmain, but still can
* have their own Action+Slot. Unfortunately there is no generic looper
* for embedded IDs. At this moment the only animatable embedded ID is a
* node tree. */
bNodeTree *node_tree = bke::node_tree_from_id(id);
if (node_tree) {
foreach_action_slot_use_with_references(node_tree->id, flag_adt);
}
}
FOREACH_MAIN_ID_END;
}
void convert_legacy_action_assignments(Main &bmain, ReportList *reports)
{
auto version_slot_assignment = [&](ID &animated_id,
bAction *dna_action,
PointerRNA &action_slot_owner_ptr,
PropertyRNA &action_slot_prop,
char *last_used_slot_identifier) {
BLI_assert(dna_action); /* Ensured by the foreach loop. */
Action &action = dna_action->wrap();
if (action.slot_array_num == 0) {
/* There's a few reasons why this Action doesn't have a slot. It could simply be a slotted
* Action without slots, or a legacy-but-not-yet-versioned Action, or it could be it is a
* _really_ old (pre-2.50) Action. The latter are upgraded in do_versions_after_setup(), but
* this function can be called earlier than that. So better gracefully skip those. */
return true;
}
/* If there is already a slot assigned, there's nothing to do here. */
PointerRNA current_slot_ptr = RNA_property_pointer_get(&action_slot_owner_ptr,
&action_slot_prop);
if (current_slot_ptr.data) {
return true;
}
/* Reset the "last used slot identifier" to the default "Legacy Slot". That way
* generic_slot_for_autoassign() will pick up on legacy slots automatically.
*
* Note that this function should only run on legacy users of Actions, i.e. they are not
* expected to have any last-used slot at all. The field in DNA can still be set, though,
* because the 4.3 code already has the data model for slotted Actions. */
/* Ensure that the identifier has the correct ID type prefix. */
*reinterpret_cast<short *>(last_used_slot_identifier) = GS(animated_id.name);
static_assert(Slot::identifier_length_max > 2); /* Because of the -2 below. */
BLI_strncpy_utf8(last_used_slot_identifier + 2,
DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME),
Slot::identifier_length_max - 2);
Slot *slot_to_assign = generic_slot_for_autoassign(
animated_id, action, last_used_slot_identifier);
if (!slot_to_assign) {
/* This means that there is no slot that can be found by name, not even the "Legacy Slot"
* name. Keep the ID unanimated, as this means that the referenced Action has changed
* significantly since this file was opened. */
BKE_reportf(reports,
RPT_WARNING,
"\"%s\" is using Action \"%s\", which does not have a slot with identifier "
"\"%s\" or \"%s\". Manually assign the right action slot to \"%s\".\n",
animated_id.name,
action.id.name + 2,
last_used_slot_identifier,
animated_id.name,
animated_id.name + 2);
return true;
}
PointerRNA slot_to_assign_ptr = RNA_pointer_create_discrete(
&action.id, RNA_ActionSlot, slot_to_assign);
RNA_property_pointer_set(
&action_slot_owner_ptr, &action_slot_prop, slot_to_assign_ptr, reports);
RNA_property_update_main(&bmain, nullptr, &action_slot_owner_ptr, &action_slot_prop);
return true;
};
/* Note that the code below does not remove the `action_assignment_needs_slot` tag. One ID can
* use multiple Actions (via NLA, Action constraints, etc.); if one of those Action is a legacy
* one from a linked datablock, this ID may needs to be re-visited after the library file was
* versioned. Rather than trying to figure out if re-visiting is necessary, this function is safe
* to call multiple times, and all that's lost is a little bit of CPU time. */
ID *id;
FOREACH_MAIN_ID_BEGIN (&bmain, id) {
/* Process the ID itself. */
if (BLO_readfile_id_runtime_tags(*id).action_assignment_needs_slot) {
foreach_action_slot_use_with_rna(*id, version_slot_assignment);
}
/* Process embedded IDs, as these are not listed in bmain, but still can
* have their own Action+Slot. Unfortunately there is no generic looper
* for embedded IDs. At this moment the only animatable embedded ID is a
* node tree. */
bNodeTree *node_tree = bke::node_tree_from_id(id);
if (node_tree && BLO_readfile_id_runtime_tags(node_tree->id).action_assignment_needs_slot) {
foreach_action_slot_use_with_rna(node_tree->id, version_slot_assignment);
}
}
FOREACH_MAIN_ID_END;
}
void action_groups_reconstruct(bAction *act)
{
if (!act) {
return;
}
/* Check that this is a legacy action.
* Cannot use `!action_is_layered` because that would be false on empty actions. */
BLI_assert(act->layer_array_num == 0 && act->slot_array_num == 0);
/* Clear out all group channels. Channels that are actually in use are
* reconstructed below; this step is necessary to clear out unused groups. */
for (bActionGroup &group : act->groups) {
group.channels.clear_no_delete();
}
/* Sort the channels into the group lists, destroying the act->curves list. */
ListBaseT<FCurve> ungrouped = {nullptr, nullptr};
for (FCurve &fcurve : act->curves.items_mutable()) {
if (fcurve.grp) {
BLI_assert(BLI_findindex(&act->groups, fcurve.grp) >= 0);
BLI_addtail(&fcurve.grp->channels, &fcurve);
}
else {
BLI_addtail(&ungrouped, &fcurve);
}
}
/* Recombine into the main list. */
act->curves.clear_no_delete();
for (bActionGroup &group : act->groups) {
/* Copy the list header to preserve the pointers in the group. */
ListBase tmp = group.channels;
BLI_movelisttolist(&act->curves, &tmp);
}
BLI_movelisttolist(&act->curves, &ungrouped);
}
using IDFCurveCallback = FunctionRef<bool(ID *, FCurve *)>;
/**
* Iterates over FCurves until the callback returns false or all FCurves were visited.
*
* \returns true if all FCurves were visited.
*/
static bool fcurves_listbase_apply_cb(ID *id,
ListBaseT<FCurve> *fcurves,
const IDFCurveCallback func)
{
for (FCurve &fcu : *fcurves) {
if (!func(id, &fcu)) {
return false;
}
}
return true;
}
/* Helper for adt_apply_all_fcurves_cb() - Recursively go through each NLA strip */
static bool nlastrips_apply_all_curves_cb(ID *id,
ListBaseT<NlaStrip> *strips,
const IDFCurveCallback func)
{
for (NlaStrip &strip : *strips) {
if (strip.act) {
if (!fcurves_listbase_apply_cb(id, &strip.act->curves, func)) {
return false;
}
}
if (!nlastrips_apply_all_curves_cb(id, &strip.strips, func)) {
return false;
}
}
return true;
}
static bool adt_apply_all_fcurves_cb(ID *id, AnimData *adt, const IDFCurveCallback func)
{
if (adt->action) {
if (!fcurves_listbase_apply_cb(id, &adt->action->curves, func)) {
return false;
}
}
if (adt->tmpact) {
if (!fcurves_listbase_apply_cb(id, &adt->tmpact->curves, func)) {
return false;
}
}
/* Drivers, stored as a list of F-Curves. */
if (!fcurves_listbase_apply_cb(id, &adt->drivers, func)) {
return false;
}
/* NLA Data - Animation Data for Strips */
for (NlaTrack &nlt : adt->nla_tracks) {
if (!nlastrips_apply_all_curves_cb(id, &nlt.strips, func)) {
return false;
}
}
return true;
}
void fcurves_id_cb(ID *id, const FunctionRef<void(ID *, FCurve *)> func)
{
AnimData *adt = BKE_animdata_from_id(id);
if (adt != nullptr) {
/* Use a little wrapper function to always return 'true' and thus keep the loop looping. */
const auto wrapper = [&func](ID *id, FCurve *fcurve) {
func(id, fcurve);
return true;
};
adt_apply_all_fcurves_cb(id, adt, wrapper);
}
}
void fcurves_main_cb(Main *bmain, const FunctionRef<void(ID *, FCurve *)> func)
{
/* Use a little wrapper function to always return 'true' and thus keep the loop looping. */
const auto wrapper = [&func](ID *id, FCurve *fcurve) {
func(id, fcurve);
return true;
};
/* Use the AnimData-based function so that we don't have to reimplement all that stuff */
BKE_animdata_main_cb(bmain,
[&](ID *id, AnimData *adt) { adt_apply_all_fcurves_cb(id, adt, wrapper); });
}
Vector<FCurve *> fcurves_for_legacy_action(bAction *action)
{
if (!action) {
return {};
}
Vector<FCurve *> fcurves;
for (FCurve &fcu : action->curves) {
fcurves.append(&fcu);
}
return fcurves;
}
} // namespace blender::animrig::versioning

View File

@@ -0,0 +1,86 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* The tests in this file need to be able to test deprecated data as well. */
#define DNA_DEPRECATED_ALLOW
#include "ANIM_versioning.hh"
#include "DNA_action_types.h"
#include "BKE_gtest_base.hh"
#include "BLI_listbase.h"
#include "testing/testing.h"
namespace blender::animrig::versioning::tests {
class AnimrigVersioninTest : public bke::BlenderGTestBase {};
TEST_F(AnimrigVersioninTest, action_is_layered)
{
/* This unit test doesn't put valid data in the action under test. Since action_is_layered()
* only looks at the length of lists, and not their contents, that should be fine. */
{ /* Animato Action only fcurves / Blender version [2.5, 4.4) */
bAction action = {};
Link /* FCurve */ fake_fcurve = {};
BLI_addtail(&action.curves, &fake_fcurve);
EXPECT_FALSE(action_is_layered(action))
<< "Animato Actions should NOT be considered 'layered'";
}
{ /* Animato Action with fcurves + groups / Blender version [2.5, 4.4) */
bAction action = {};
Link /* FCurve */ fake_fcurve = {};
Link /* bActionGroup */ fake_group = {};
BLI_addtail(&action.curves, &fake_fcurve);
BLI_addtail(&action.groups, &fake_group);
EXPECT_FALSE(action_is_layered(action))
<< "Animato Actions should NOT be considered 'layered'";
}
{ /* Animato Action with only groups / Blender version [2.5, 4.4) */
bAction action = {};
Link /* bActionGroup */ fake_group = {};
BLI_addtail(&action.groups, &fake_group);
EXPECT_FALSE(action_is_layered(action))
<< "Animato Actions should NOT be considered 'layered'";
}
{ /* Layered Action with only layers / Blender version 4.4 and newer. */
bAction action = {};
action.layer_array_num = 1;
EXPECT_TRUE(action_is_layered(action)) << "Layered Actions should be considered 'layered'";
}
{ /* Layered Action with only slots / Blender version 4.4 and newer. */
bAction action = {};
action.slot_array_num = 1;
EXPECT_TRUE(action_is_layered(action)) << "Layered Actions should be considered 'layered'";
}
{ /* Layered Action as it exists on disk, with forward-compatible info in there. */
bAction action = {};
Link /* FCurve */ fake_fcurve = {};
action.layer_array_num = 1;
BLI_addtail(&action.curves, &fake_fcurve);
EXPECT_TRUE(action_is_layered(action))
<< "Layered Actions with forward-compat data should be considered 'layered'";
}
{ /* Completely zeroed out Action. */
bAction action = {};
EXPECT_TRUE(action_is_layered(action)) << "Zero'ed-out Actions should be considered 'layered'";
}
}
} // namespace blender::animrig::versioning::tests

View File

@@ -0,0 +1,272 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include <cstdio>
#include <cstring>
#include "ANIM_rna.hh"
#include "ANIM_visualkey.hh"
#include "BKE_armature.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "DNA_constraint_types.h"
#include "DNA_object_types.h"
#include "DNA_rigidbody_types.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
namespace blender::animrig {
/* Internal status codes for visualkey_can_use. */
enum {
VISUALKEY_NONE = 0,
VISUALKEY_LOC,
VISUALKEY_ROT,
VISUALKEY_SCA,
};
bool visualkey_can_use(PointerRNA *ptr, PropertyRNA *prop)
{
bConstraint *con = nullptr;
bool has_rigidbody = false;
bool has_parent = false;
if (ELEM(nullptr, ptr, ptr->data, prop)) {
return false;
}
/* Get first constraint and determine type of keyframe constraints to check for
* - constraints can be on either Objects or PoseChannels, so we only check if the
* ptr->type is RNA_Object or RNA_PoseBone, which are the RNA wrapping-info for
* those structs, allowing us to identify the owner of the data
*/
if (ptr->type == RNA_Object) {
Object *ob = static_cast<Object *>(ptr->data);
RigidBodyOb *rbo = ob->rigidbody_object;
con = static_cast<bConstraint *>(ob->constraints.first);
has_parent = (ob->parent != nullptr);
/* Active rigidbody objects only, as only those are affected by sim. */
has_rigidbody = ((rbo) && (rbo->type == RBO_TYPE_ACTIVE));
}
else if (ptr->type == RNA_PoseBone) {
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr->data);
if (pchan->constflag & (PCHAN_HAS_IK | PCHAN_INFLUENCED_BY_IK)) {
/* Spline IK cannot generally be keyed visually, because (at least with the default
* constraint settings) it requires non-uniform scaling that causes shearing in child bones,
* which cannot be represented by the bone's loc/rot/scale properties. */
return true;
}
con = static_cast<bConstraint *>(pchan->constraints.first);
has_parent = (pchan->parent != nullptr);
}
else {
BLI_assert_msg(false,
"visualkey_can_use called for data-block that is not an Object or PoseBone.");
return false;
}
/* Parent or rigidbody are always matching, no need to check further. */
if (has_parent || has_rigidbody) {
return true;
}
/* Only do visual keying on transforms. */
const char *identifier = RNA_property_identifier(prop);
if (identifier == nullptr) {
printf("%s failed: nullptr identifier\n", __func__);
return false;
}
short searchtype = VISUALKEY_NONE;
if (strstr(identifier, "location")) {
searchtype = VISUALKEY_LOC;
}
else if (strstr(identifier, "rotation")) {
searchtype = VISUALKEY_ROT;
}
else if (strstr(identifier, "scale")) {
searchtype = VISUALKEY_SCA;
}
else {
printf("%s failed: identifier - '%s'\n", __func__, identifier);
return false;
}
/* Check constraints. */
for (; con; con = con->next) {
/* only consider constraint if it is not disabled, and has influence */
if (con->flag & CONSTRAINT_DISABLE) {
continue;
}
if (con->enforce == 0.0f) {
continue;
}
/* Some constraints may alter these transforms. */
switch (con->type) {
/* Multi-transform constraints. */
case CONSTRAINT_TYPE_CHILDOF:
case CONSTRAINT_TYPE_ARMATURE:
return true;
case CONSTRAINT_TYPE_TRANSFORM:
case CONSTRAINT_TYPE_TRANSLIKE:
return true;
case CONSTRAINT_TYPE_FOLLOWPATH:
return true;
case CONSTRAINT_TYPE_KINEMATIC:
return true;
/* Single-transform constraints. */
case CONSTRAINT_TYPE_TRACKTO:
if (searchtype == VISUALKEY_ROT) {
return true;
}
break;
case CONSTRAINT_TYPE_DAMPTRACK:
if (searchtype == VISUALKEY_ROT) {
return true;
}
break;
case CONSTRAINT_TYPE_ROTLIMIT:
if (searchtype == VISUALKEY_ROT) {
return true;
}
break;
case CONSTRAINT_TYPE_LOCLIMIT:
if (searchtype == VISUALKEY_LOC) {
return true;
}
break;
case CONSTRAINT_TYPE_SIZELIMIT:
if (searchtype == VISUALKEY_SCA) {
return true;
}
break;
case CONSTRAINT_TYPE_DISTLIMIT:
if (searchtype == VISUALKEY_LOC) {
return true;
}
break;
case CONSTRAINT_TYPE_ROTLIKE:
if (searchtype == VISUALKEY_ROT) {
return true;
}
break;
case CONSTRAINT_TYPE_LOCLIKE:
if (searchtype == VISUALKEY_LOC) {
return true;
}
break;
case CONSTRAINT_TYPE_SIZELIKE:
if (searchtype == VISUALKEY_SCA) {
return true;
}
break;
case CONSTRAINT_TYPE_LOCKTRACK:
if (searchtype == VISUALKEY_ROT) {
return true;
}
break;
case CONSTRAINT_TYPE_MINMAX:
if (searchtype == VISUALKEY_LOC) {
return true;
}
break;
default:
break;
}
}
return false;
}
Vector<float> visualkey_get_values(PointerRNA *ptr, PropertyRNA *prop)
{
Vector<float> values;
const char *identifier = RNA_property_identifier(prop);
float tmat[4][4];
int rotmode;
/* Handle for Objects or PoseChannels only
* - only Location, Rotation or Scale keyframes are supported currently
* - constraints can be on either Objects or PoseChannels, so we only check if the
* ptr->type is RNA_Object or RNA_PoseBone, which are the RNA wrapping-info for
* those structs, allowing us to identify the owner of the data
* - assume that array_index will be sane
*/
if (ptr->type == RNA_Object) {
Object *ob = static_cast<Object *>(ptr->data);
/* Loc code is specific... */
if (strstr(identifier, "location")) {
values.extend({ob->object_to_world().location(), 3});
return values;
}
copy_m4_m4(tmat, ob->object_to_world().ptr());
rotmode = ob->rotmode;
}
else if (ptr->type == RNA_PoseBone) {
Object *ob = id_cast<Object *>(ptr->owner_id);
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr->data);
Bone *bone = pchan->bone_get(*ob);
BKE_armature_mat_pose_to_bone({pchan, bone}, pchan->pose_mat, tmat);
rotmode = pchan->rotmode;
/* Loc code is specific... */
if (strstr(identifier, "location")) {
/* Only use for non-connected bones. */
if ((bone->parent == nullptr) || !(bone->flag & BONE_CONNECTED)) {
values.extend({tmat[3], 3});
return values;
}
}
}
else {
return get_rna_values(ptr, prop);
}
/* Rot/Scale code are common! */
if (strstr(identifier, "rotation_euler")) {
values.resize(3);
mat4_to_eulO(values.data(), rotmode, tmat);
return values;
}
if (strstr(identifier, "rotation_quaternion")) {
values.resize(4);
mat4_to_quat(values.data(), tmat);
return values;
}
if (strstr(identifier, "rotation_axis_angle")) {
/* w = 0, x,y,z = 1,2,3 */
values.resize(4);
mat4_to_axis_angle(values.data() + 1, values.data() + 0, tmat);
return values;
}
if (strstr(identifier, "scale")) {
values.resize(3);
mat4_to_size(values.data(), tmat);
return values;
}
/* As the function hasn't returned yet, read value from system in the default way. */
return get_rna_values(ptr, prop);
}
} // namespace blender::animrig