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
This commit is contained in:
committed by
Christoph Lendenfeld
parent
51f94d6234
commit
77f75353b0
@@ -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.
|
||||
*
|
||||
|
||||
40
source/blender/animrig/ANIM_action_iterators.hh
Normal file
40
source/blender/animrig/ANIM_action_iterators.hh
Normal file
@@ -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 <cstdint>
|
||||
|
||||
#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<void(FCurve &fcurve)> callback);
|
||||
|
||||
} // namespace blender::animrig
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<KeyframeStrip>();
|
||||
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,
|
||||
|
||||
39
source/blender/animrig/intern/action_iterators.cc
Normal file
39
source/blender/animrig/intern/action_iterators.cc
Normal file
@@ -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<void(FCurve &fcurve)> callback)
|
||||
{
|
||||
BLI_assert(action.is_action_layered());
|
||||
for (Layer *layer : action.layers()) {
|
||||
for (Strip *strip : layer->strips()) {
|
||||
if (!strip->is<KeyframeStrip>()) {
|
||||
continue;
|
||||
}
|
||||
KeyframeStrip &key_strip = strip->as<KeyframeStrip>();
|
||||
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
|
||||
110
source/blender/animrig/intern/action_iterators_test.cc
Normal file
110
source/blender/animrig/intern/action_iterators_test.cc
Normal file
@@ -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<Action *>(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<FCurve *> 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<KeyframeStrip>();
|
||||
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<FCurve *> 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<FCurve *> 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<FCurve *> 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#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"
|
||||
@@ -670,13 +671,13 @@ int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna
|
||||
}
|
||||
|
||||
Action &action = act->wrap();
|
||||
Vector<FCurve *> 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<FCurve *> 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<FCurve *> 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);
|
||||
}
|
||||
|
||||
@@ -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<FCurve *>(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<FCurve *> 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<FCurve *>(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<FCurve *> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user