From 77f75353b05cd797ca955002ecfdd765b8bbec3d Mon Sep 17 00:00:00 2001 From: Christoph Lendenfeld Date: Thu, 8 Aug 2024 11:06:49 +0200 Subject: [PATCH] Anim: Remove empty FCurves from layered Actions This changes the behavior when deleting the last key of an FCurve on layered actions. Previously the FCurve would continue to exist, whereas now it is deleted. This makes it consistent with legacy actions. I modified the "Clear Keyframes" operator in this PR as well to make it work with layered actions. Pull Request: https://projects.blender.org/blender/blender/pulls/125327 --- source/blender/animrig/ANIM_action.hh | 8 + .../blender/animrig/ANIM_action_iterators.hh | 40 +++++ source/blender/animrig/ANIM_fcurve.hh | 2 + source/blender/animrig/CMakeLists.txt | 3 + source/blender/animrig/intern/action.cc | 19 +++ .../animrig/intern/action_iterators.cc | 39 +++++ .../animrig/intern/action_iterators_test.cc | 110 +++++++++++++ source/blender/animrig/intern/animdata.cc | 4 +- source/blender/animrig/intern/fcurve.cc | 3 + source/blender/animrig/intern/keyframing.cc | 149 +++++++++++------- .../blender/editors/animation/keyframing.cc | 107 ++++++++----- tests/python/bl_animation_keyframing.py | 36 +++++ 12 files changed, 424 insertions(+), 96 deletions(-) create mode 100644 source/blender/animrig/ANIM_action_iterators.hh create mode 100644 source/blender/animrig/intern/action_iterators.cc create mode 100644 source/blender/animrig/intern/action_iterators_test.cc diff --git a/source/blender/animrig/ANIM_action.hh b/source/blender/animrig/ANIM_action.hh index 61c0a35ec6f..455d7f32739 100644 --- a/source/blender/animrig/ANIM_action.hh +++ b/source/blender/animrig/ANIM_action.hh @@ -892,6 +892,14 @@ FCurve *action_fcurve_ensure(Main *bmain, */ FCurve *action_fcurve_find(bAction *act, FCurveDescriptor fcurve_descriptor); +/** + * Remove the given FCurve from the action by searching for it in all channelbags. + * This assumes that an FCurve can only exist in an action once. + * + * \returns true if the given FCurve was removed. + */ +bool action_fcurve_remove(Action &action, FCurve &fcu); + /** * Find an appropriate user of the given Action + Slot for keyframing purposes. * diff --git a/source/blender/animrig/ANIM_action_iterators.hh b/source/blender/animrig/ANIM_action_iterators.hh new file mode 100644 index 00000000000..2a3424f4af3 --- /dev/null +++ b/source/blender/animrig/ANIM_action_iterators.hh @@ -0,0 +1,40 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup animrig + * + * \brief Functionality to iterate an Action in various ways. + */ + +#pragma once + +#include + +#include "BLI_vector.hh" +#include "DNA_action_types.h" + +struct FCurve; +namespace blender::animrig { +class Action; +class Layer; +class Strip; +class ChannelBag; +} // namespace blender::animrig + +namespace blender::animrig { + +using slot_handle_t = decltype(::ActionSlot::handle); + +/** + * Iterates over all FCurves of the given slot handle in the Action and executes the callback on + * it. Only works on layered Actions. + * + * \note Use lambdas to have access to specific data in the callback. + */ +void action_foreach_fcurve(Action &action, + slot_handle_t handle, + FunctionRef callback); + +} // namespace blender::animrig diff --git a/source/blender/animrig/ANIM_fcurve.hh b/source/blender/animrig/ANIM_fcurve.hh index 62976322adb..6afdfd24b09 100644 --- a/source/blender/animrig/ANIM_fcurve.hh +++ b/source/blender/animrig/ANIM_fcurve.hh @@ -71,6 +71,8 @@ void initialize_bezt(BezTriple *beztr, /** * Delete the keyframe at `time` on `fcurve` if a key exists there. * + * This does NOT delete the FCurve if it ends up empty. That is for the caller to do. + * * \note `time` is in fcurve time, not scene time. Any time remapping must be * done prior to calling this function. * diff --git a/source/blender/animrig/CMakeLists.txt b/source/blender/animrig/CMakeLists.txt index beeb22874ec..f81427616b4 100644 --- a/source/blender/animrig/CMakeLists.txt +++ b/source/blender/animrig/CMakeLists.txt @@ -24,6 +24,7 @@ set(SRC intern/action_legacy.cc intern/action_runtime.cc intern/action_selection.cc + intern/action_iterators.cc intern/anim_rna.cc intern/animdata.cc intern/bone_collections.cc @@ -37,6 +38,7 @@ set(SRC ANIM_action.hh ANIM_action_legacy.hh + ANIM_action_iterators.hh ANIM_animdata.hh ANIM_armature_iter.hh ANIM_bone_collections.hh @@ -76,6 +78,7 @@ if(WITH_GTESTS) ) set(TEST_SRC intern/action_test.cc + intern/action_iterators_test.cc intern/bone_collections_test.cc intern/evaluation_test.cc intern/keyframing_test.cc diff --git a/source/blender/animrig/intern/action.cc b/source/blender/animrig/intern/action.cc index c1c2ce4a72f..c7ebd6e34c9 100644 --- a/source/blender/animrig/intern/action.cc +++ b/source/blender/animrig/intern/action.cc @@ -1530,6 +1530,25 @@ FCurve *action_fcurve_ensure(Main *bmain, return fcu; } +bool action_fcurve_remove(Action &action, FCurve &fcu) +{ + for (Layer *layer : action.layers()) { + for (Strip *strip : layer->strips()) { + if (!(strip->type() == Strip::Type::Keyframe)) { + continue; + } + KeyframeStrip &key_strip = strip->template as(); + for (ChannelBag *bag : key_strip.channelbags()) { + const bool removed = bag->fcurve_remove(fcu); + if (removed) { + return true; + } + } + } + } + return false; +} + ID *action_slot_get_id_for_keying(Main &bmain, Action &action, const slot_handle_t slot_handle, diff --git a/source/blender/animrig/intern/action_iterators.cc b/source/blender/animrig/intern/action_iterators.cc new file mode 100644 index 00000000000..f78004bc152 --- /dev/null +++ b/source/blender/animrig/intern/action_iterators.cc @@ -0,0 +1,39 @@ +/* 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" + +namespace blender::animrig { + +void action_foreach_fcurve(Action &action, + slot_handle_t handle, + FunctionRef callback) +{ + BLI_assert(action.is_action_layered()); + for (Layer *layer : action.layers()) { + for (Strip *strip : layer->strips()) { + if (!strip->is()) { + continue; + } + KeyframeStrip &key_strip = strip->as(); + for (ChannelBag *bag : key_strip.channelbags()) { + if (bag->slot_handle != handle) { + continue; + } + for (FCurve *fcu : bag->fcurves()) { + BLI_assert(fcu != nullptr); + callback(*fcu); + } + } + } + } +} + +} // namespace blender::animrig diff --git a/source/blender/animrig/intern/action_iterators_test.cc b/source/blender/animrig/intern/action_iterators_test.cc new file mode 100644 index 00000000000..9164ee7eaae --- /dev/null +++ b/source/blender/animrig/intern/action_iterators_test.cc @@ -0,0 +1,110 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "ANIM_action.hh" +#include "ANIM_action_iterators.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 "CLG_log.h" +#include "testing/testing.h" + +namespace blender::animrig::tests { +class ActionIteratorsTest : public testing::Test { + public: + Main *bmain; + Action *action; + + static void SetUpTestSuite() + { + /* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */ + CLG_init(); + + /* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */ + BKE_idtype_init(); + } + + static void TearDownTestSuite() + { + CLG_exit(); + } + + void SetUp() override + { + bmain = BKE_main_new(); + action = static_cast(BKE_id_new(bmain, ID_AC, "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(); + EXPECT_TRUE(action->is_action_layered()); + + /* Try iterating an empty action. */ + blender::Vector no_fcurves; + action_foreach_fcurve( + *action, cube_slot.handle, [&](FCurve &fcurve) { no_fcurves.append(&fcurve); }); + + ASSERT_TRUE(no_fcurves.is_empty()); + + Layer &layer = action->layer_add("Layer One"); + Strip &strip = layer.strip_add(Strip::Type::Keyframe); + KeyframeStrip &key_strip = strip.as(); + const KeyframeSettings settings = get_keyframe_settings(false); + + /* Insert 3 FCurves for each slot. */ + for (int i = 0; i < 3; i++) { + SingleKeyingResult result_cube = key_strip.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 = key_strip.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. */ + blender::Vector cube_fcurves; + action_foreach_fcurve( + *action, cube_slot.handle, [&](FCurve &fcurve) { cube_fcurves.append(&fcurve); }); + + ASSERT_EQ(cube_fcurves.size(), 3); + for (FCurve *fcurve : cube_fcurves) { + ASSERT_STREQ(fcurve->rna_path, "location"); + } + + /* Get only FCurves with index 0 which should be 1. */ + blender::Vector monkey_fcurves; + action_foreach_fcurve(*action, monkey_slot.handle, [&](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. */ + blender::Vector invalid_slot_fcurves; + action_foreach_fcurve(*action, monkey_slot.handle + cube_slot.handle, [&](FCurve &fcurve) { + invalid_slot_fcurves.append(&fcurve); + }); + ASSERT_TRUE(invalid_slot_fcurves.is_empty()); +} +} // namespace blender::animrig::tests diff --git a/source/blender/animrig/intern/animdata.cc b/source/blender/animrig/intern/animdata.cc index 5048092f138..f5d7f26f28e 100644 --- a/source/blender/animrig/intern/animdata.cc +++ b/source/blender/animrig/intern/animdata.cc @@ -127,7 +127,9 @@ void animdata_fcurve_delete(bAnimContext *ac, AnimData *adt, FCurve *fcu) animdata_remove_empty_action(adt); } else { - /* TODO: support deleting FCurves from layered Actions. */ + 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; } } diff --git a/source/blender/animrig/intern/fcurve.cc b/source/blender/animrig/intern/fcurve.cc index 3c4cd86783c..72da5a4beb8 100644 --- a/source/blender/animrig/intern/fcurve.cc +++ b/source/blender/animrig/intern/fcurve.cc @@ -88,6 +88,9 @@ FCurve *create_fcurve_for_channel(const FCurveDescriptor fcurve_descriptor) bool fcurve_delete_keyframe_at_time(FCurve *fcurve, const float time) { + if (BKE_fcurve_is_protected(fcurve)) { + return false; + } bool found; const int index = BKE_fcurve_bezt_binarysearch_index( diff --git a/source/blender/animrig/intern/keyframing.cc b/source/blender/animrig/intern/keyframing.cc index 08390d9ebda..2dcff26974f 100644 --- a/source/blender/animrig/intern/keyframing.cc +++ b/source/blender/animrig/intern/keyframing.cc @@ -12,6 +12,7 @@ #include #include "ANIM_action.hh" +#include "ANIM_action_iterators.hh" #include "ANIM_animdata.hh" #include "ANIM_fcurve.hh" #include "ANIM_keyframing.hh" @@ -670,13 +671,13 @@ int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna } Action &action = act->wrap(); + Vector modified_fcurves; if (action.is_action_layered()) { /* 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 fcurves = fcurves_for_action_slot(action, adt->slot_handle); - int removed_key_count = 0; /* 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 @@ -686,38 +687,46 @@ int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna if (fcurve == nullptr) { continue; } - removed_key_count += fcurve_delete_keyframe_at_time(fcurve, cfra); + if (fcurve_delete_keyframe_at_time(fcurve, cfra)) { + modified_fcurves.append(fcurve); + } } + } + else { + /* Will only loop once unless the array index was -1. */ + for (; array_index < array_index_max; array_index++) { + FCurve *fcu = action_fcurve_find(act, {rna_path.path, array_index}); - return removed_key_count; + if (fcu == nullptr) { + continue; + } + + if (BKE_fcurve_is_protected(fcu)) { + BKE_reportf(reports, + RPT_WARNING, + "Not deleting keyframe for locked F-Curve '%s' for %s '%s'", + fcu->rna_path, + BKE_idtype_idcode_to_name(GS(id->name)), + id->name + 2); + continue; + } + + if (fcurve_delete_keyframe_at_time(fcu, cfra)) { + modified_fcurves.append(fcu); + } + } } - /* Will only loop once unless the array index was -1. */ - int key_count = 0; - for (; array_index < array_index_max; array_index++) { - FCurve *fcu = action_fcurve_find(act, {rna_path.path, array_index}); - - if (fcu == nullptr) { - continue; + if (!modified_fcurves.is_empty()) { + for (FCurve *fcurve : modified_fcurves) { + if (BKE_fcurve_is_empty(fcurve)) { + animdata_fcurve_delete(nullptr, adt, fcurve); + } } - - if (BKE_fcurve_is_protected(fcu)) { - BKE_reportf(reports, - RPT_WARNING, - "Not deleting keyframe for locked F-Curve '%s' for %s '%s'", - fcu->rna_path, - BKE_idtype_idcode_to_name(GS(id->name)), - id->name + 2); - continue; - } - - key_count += delete_keyframe_fcurve_legacy(adt, fcu, cfra); - } - if (key_count) { deg_tag_after_keyframe_delete(bmain, id, adt); } - return key_count; + return modified_fcurves.size(); } /* ************************************************** */ @@ -751,43 +760,67 @@ int clear_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_ } bAction *act = adt->action; - 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(); int key_count = 0; - /* Will only loop once unless the array index was -1. */ - for (; array_index < array_index_max; array_index++) { - FCurve *fcu = action_fcurve_find(act, {rna_path.path, array_index}); - if (fcu == nullptr) { - continue; + if (action.is_action_layered()) { + if (adt->slot_handle) { + Vector fcurves; + action_foreach_fcurve(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 (BKE_fcurve_is_protected(fcu)) { - BKE_reportf(reports, - RPT_WARNING, - "Not clearing all keyframes from locked F-Curve '%s' for %s '%s'", - fcu->rna_path, - BKE_idtype_idcode_to_name(GS(id->name)), - id->name + 2); - continue; - } - - animdata_fcurve_delete(nullptr, adt, fcu); - - key_count++; } + else { + 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++; + } + } + /* Will only loop once unless the array index was -1. */ + for (; array_index < array_index_max; array_index++) { + FCurve *fcu = action_fcurve_find(act, {rna_path.path, array_index}); + + if (fcu == nullptr) { + continue; + } + + if (BKE_fcurve_is_protected(fcu)) { + BKE_reportf(reports, + RPT_WARNING, + "Not clearing all keyframes from locked F-Curve '%s' for %s '%s'", + fcu->rna_path, + BKE_idtype_idcode_to_name(GS(id->name)), + id->name + 2); + continue; + } + + animdata_fcurve_delete(nullptr, adt, fcu); + + key_count++; + } + } + if (key_count) { deg_tag_after_keyframe_delete(bmain, id, adt); } diff --git a/source/blender/editors/animation/keyframing.cc b/source/blender/editors/animation/keyframing.cc index 1109c2784f5..4aef81c3342 100644 --- a/source/blender/editors/animation/keyframing.cc +++ b/source/blender/editors/animation/keyframing.cc @@ -45,6 +45,7 @@ #include "ED_screen.hh" #include "ANIM_action.hh" +#include "ANIM_action_iterators.hh" #include "ANIM_animdata.hh" #include "ANIM_bone_collections.hh" #include "ANIM_driver.hh" @@ -708,50 +709,70 @@ void ANIM_OT_keyframe_delete_by_name(wmOperatorType *ot) * it is more useful for animators working in the 3D view. */ +/* While in pose mode, the selection of bones has to be considered. */ +static bool can_delete_fcurve(FCurve *fcu, Object *ob) +{ + bool can_delete = false; + /* in pose mode, only delete the F-Curve if it belongs to a selected bone */ + if (ob->mode & OB_MODE_POSE) { + if (fcu->rna_path) { + /* Get bone-name, and check if this bone is selected. */ + bPoseChannel *pchan = nullptr; + char bone_name[sizeof(pchan->name)]; + if (BLI_str_quoted_substr(fcu->rna_path, "pose.bones[", bone_name, sizeof(bone_name))) { + pchan = BKE_pose_channel_find_name(ob->pose, bone_name); + /* Delete if bone is selected. */ + if ((pchan) && (pchan->bone)) { + if (pchan->bone->flag & BONE_SELECTED) { + can_delete = true; + } + } + } + } + } + else { + /* object mode - all of Object's F-Curves are affected */ + /* TODO: this logic isn't solid. Only delete FCurves of the object, not of bones in this case. + */ + can_delete = true; + } + + return can_delete; +} + static int clear_anim_v3d_exec(bContext *C, wmOperator * /*op*/) { + using namespace blender::animrig; bool changed = false; CTX_DATA_BEGIN (C, Object *, ob, selected_objects) { /* just those in active action... */ if ((ob->adt) && (ob->adt->action)) { AnimData *adt = ob->adt; - bAction *act = adt->action; + bAction *dna_action = adt->action; FCurve *fcu, *fcn; - for (fcu = static_cast(act->curves.first); fcu; fcu = fcn) { - bool can_delete = false; - - fcn = fcu->next; - - /* in pose mode, only delete the F-Curve if it belongs to a selected bone */ - if (ob->mode & OB_MODE_POSE) { - if (fcu->rna_path) { - /* Get bone-name, and check if this bone is selected. */ - bPoseChannel *pchan = nullptr; - char bone_name[sizeof(pchan->name)]; - if (BLI_str_quoted_substr(fcu->rna_path, "pose.bones[", bone_name, sizeof(bone_name))) - { - pchan = BKE_pose_channel_find_name(ob->pose, bone_name); - /* Delete if bone is selected. */ - if ((pchan) && (pchan->bone)) { - if (pchan->bone->flag & BONE_SELECTED) { - can_delete = true; - } - } - } + Action &action = dna_action->wrap(); + if (action.is_action_layered()) { + blender::Vector fcurves_to_delete; + action_foreach_fcurve(action, adt->slot_handle, [&](FCurve &fcurve) { + if (can_delete_fcurve(&fcurve, ob)) { + fcurves_to_delete.append(&fcurve); } + }); + for (FCurve *fcurve : fcurves_to_delete) { + action_fcurve_remove(action, *fcurve); } - else { - /* object mode - all of Object's F-Curves are affected */ - can_delete = true; - } - - /* delete F-Curve completely */ - if (can_delete) { - blender::animrig::animdata_fcurve_delete(nullptr, adt, fcu); - DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM); - changed = true; + } + else { + for (fcu = static_cast(dna_action->curves.first); fcu; fcu = fcn) { + fcn = fcu->next; + /* delete F-Curve completely */ + if (can_delete_fcurve(fcu, ob)) { + blender::animrig::animdata_fcurve_delete(nullptr, adt, fcu); + DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM); + changed = true; + } } } @@ -820,6 +841,8 @@ static bool can_delete_key(FCurve *fcu, Object *ob, ReportList *reports) /* Special exception for bones, as this makes this operator more convenient to use * NOTE: This is only done in pose mode. * In object mode, we're dealing with the entire object. + * TODO: While this means bone animation is not deleted of all bones while in pose mode. Running + * the code on the armature object WILL delete keys of all bones. */ if (ob->mode & OB_MODE_POSE) { bPoseChannel *pchan = nullptr; @@ -880,11 +903,21 @@ static int delete_key_v3d_without_keying_set(bContext *C, wmOperator *op) Action &action = act->wrap(); if (action.is_action_layered()) { - for (FCurve *fcu : fcurves_for_action_slot(action, adt->slot_handle)) { - if (!can_delete_key(fcu, ob, op->reports)) { - continue; + blender::Vector modified_fcurves; + action_foreach_fcurve(action, adt->slot_handle, [&](FCurve &fcurve) { + if (!can_delete_key(&fcurve, ob, op->reports)) { + return; + } + if (blender::animrig::fcurve_delete_keyframe_at_time(&fcurve, cfra_unmap)) { + modified_fcurves.append(&fcurve); + } + }); + + success += modified_fcurves.size(); + for (FCurve *fcurve : modified_fcurves) { + if (BKE_fcurve_is_empty(fcurve)) { + action_fcurve_remove(action, *fcurve); } - success += blender::animrig::fcurve_delete_keyframe_at_time(fcu, cfra_unmap); } } else { @@ -897,7 +930,7 @@ static int delete_key_v3d_without_keying_set(bContext *C, wmOperator *op) /* Delete keyframes on current frame * WARNING: this can delete the next F-Curve, hence the "fcn" copying. */ - success += blender::animrig::delete_keyframe_fcurve_legacy(adt, fcu, cfra_unmap); + success += delete_keyframe_fcurve_legacy(adt, fcu, cfra_unmap); } } diff --git a/tests/python/bl_animation_keyframing.py b/tests/python/bl_animation_keyframing.py index f571f5e4183..dee531a0d0e 100644 --- a/tests/python/bl_animation_keyframing.py +++ b/tests/python/bl_animation_keyframing.py @@ -688,6 +688,42 @@ class NlaInsertTest(AbstractKeyframingTest, unittest.TestCase): self.assertAlmostEqual(fcurve_loc_x.keyframe_points[-1].co[1], 1.0, 8) +class KeyframeDeleteTest(AbstractKeyframingTest, unittest.TestCase): + + def test_delete_in_v3d_pose_mode(self): + armature = _create_armature() + bpy.context.scene.frame_set(1) + with bpy.context.temp_override(**_get_view3d_context()): + bpy.ops.anim.keyframe_insert_by_name(type="Location") + self.assertTrue(armature.animation_data is not None) + self.assertTrue(armature.animation_data.action is not None) + action = armature.animation_data.action + self.assertEqual(len(action.fcurves), 3) + + bpy.ops.object.mode_set(mode='POSE') + with bpy.context.temp_override(**_get_view3d_context()): + bpy.ops.anim.keyframe_insert_by_name(type="Location") + bpy.context.scene.frame_set(5) + bpy.ops.anim.keyframe_insert_by_name(type="Location") + # This should have added new FCurves for the pose bone. + self.assertEqual(len(action.fcurves), 6) + + bpy.ops.anim.keyframe_delete_v3d() + # No Fcurves should yet be deleted. + self.assertEqual(len(action.fcurves), 6) + self.assertEqual(len(action.fcurves[0].keyframe_points), 1) + bpy.context.scene.frame_set(1) + bpy.ops.anim.keyframe_delete_v3d() + # This should leave the object level keyframes of the armature + self.assertEqual(len(action.fcurves), 3) + + bpy.ops.object.mode_set(mode='OBJECT') + with bpy.context.temp_override(**_get_view3d_context()): + bpy.ops.anim.keyframe_delete_v3d() + # The last FCurves should be deleted from the object now. + self.assertEqual(len(action.fcurves), 0) + + def main(): global args import argparse