Anim: Detailed report if no keyframes have been inserted #119201

Merged
Christoph Lendenfeld merged 13 commits from ChrisLend/blender:return_keying_result into main 2024-04-09 09:39:25 +02:00
4 changed files with 218 additions and 158 deletions

View File

@ -12,6 +12,7 @@
#include <string>
#include "BLI_array.hh"
#include "BLI_bit_span.hh"
#include "BLI_vector.hh"
#include "DNA_anim_types.h"
@ -29,6 +30,41 @@ struct NlaKeyframingContext;
namespace blender::animrig {
enum class SingleKeyingResult {
SUCCESS = 0,
CANNOT_CREATE_FCURVE,
FCURVE_NOT_KEYFRAMEABLE,
NO_KEY_NEEDED,
UNABLE_TO_INSERT_TO_NLA_STACK,
/* Make sure to always keep this at the end of the enum. */
_KEYING_RESULT_MAX,
};
/**
* Class for tracking the result of inserting keyframes. Tracks how often each of
* `SingleKeyingResult` has happened.
* */
class CombinedKeyingResult {
private:
/* The index to the array maps a `SingleKeyingResult` to the number of times this result has
* occurred. */
Array<int> result_counter;
public:
CombinedKeyingResult();
void add(const SingleKeyingResult result);
/* Add values of the given result to this result. */
void merge(const CombinedKeyingResult &combined_result);
int get_count(const SingleKeyingResult result) const;
bool has_errors() const;
void generate_reports(ReportList *reports);
};
/* -------------------------------------------------------------------- */
/** \name Key-Framing Management
* \{ */
@ -194,31 +230,31 @@ bool autokeyframe_property(bContext *C,
* already.
* \param keying_mask is expected to have the same size as `rna_path`. A false bit means that index
* will be skipped.
* \returns The number of keys inserted.
* \returns How often keyframe insertion was successful and how often it failed / for which reason.
*/

How often keyframe insertion was successful and how often it failed / for which reason.

Same for the copies below, of course.

How often keyframe insertion was successful `and` how often it failed `/` for which reason. Same for the copies below, of course.
int insert_key_action(Main *bmain,
bAction *action,
PointerRNA *ptr,
PropertyRNA *prop,
const std::string &rna_path,
float frame,
Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type,
BitSpan keying_mask);
CombinedKeyingResult insert_key_action(Main *bmain,
bAction *action,
PointerRNA *ptr,
PropertyRNA *prop,
const std::string &rna_path,
float frame,
Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type,
BitSpan keying_mask);
/**
* Insert keys to the ID of the given PointerRNA for the given RNA paths. Tries to create an
* action if none exists yet.
* \param scene_frame: is expected to be not NLA mapped as that happens within the function.
* \returns How often keyframe insertion was successful and how often it failed / for which reason.
*/
void insert_key_rna(PointerRNA *rna_pointer,
const blender::Span<std::string> rna_paths,
float scene_frame,
eInsertKeyFlags insert_key_flags,
eBezTriple_KeyframeType key_type,
Main *bmain,
ReportList *reports,
const AnimationEvalContext &anim_eval_context);
CombinedKeyingResult insert_key_rna(PointerRNA *rna_pointer,
const blender::Span<std::string> rna_paths,
float scene_frame,
eInsertKeyFlags insert_key_flags,
eBezTriple_KeyframeType key_type,
Main *bmain,
const AnimationEvalContext &anim_eval_context);
} // namespace blender::animrig

View File

@ -48,44 +48,102 @@
namespace blender::animrig {
enum class SingleKeyingResult {
SUCCESS = 0,
CANNOT_CREATE_FCURVE,
FCURVE_NOT_KEYFRAMEABLE,
NO_KEY_NEEDED,
/* Make sure to always keep this at the end of the enum. */
_KEYING_RESULT_MAX,
};
CombinedKeyingResult::CombinedKeyingResult()
{
result_counter = Array<int>(int(SingleKeyingResult::_KEYING_RESULT_MAX));
result_counter.fill(0);
}
class CombinedKeyingResult {
private:
/* The index to the array maps a `SingleKeyingResult` to the number of times this result has
* occurred. */
std::array<int, int(SingleKeyingResult::_KEYING_RESULT_MAX)> result_counter{0};
void CombinedKeyingResult::add(const SingleKeyingResult result)
{
result_counter[int(result)]++;
}
public:
void add(const SingleKeyingResult result)
{
result_counter[int(result)]++;
void CombinedKeyingResult::merge(const CombinedKeyingResult &other)
{
for (int i = 0; i < result_counter.size(); i++) {
result_counter[i] += other.result_counter[i];
}
}
int get_count(const SingleKeyingResult result) const
{
return result_counter[int(result)];
}
int CombinedKeyingResult::get_count(const SingleKeyingResult result) const
{
return result_counter[int(result)];
}
bool 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. */
for (int i = 1; i < result_counter.size(); i++) {
if (result_counter[i] > 0) {
return true;
}
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 a future improvement: adding a static_assert(SingleKeyingResult::SUCCESS == 0) could be a nice touch here.

For a future improvement: adding a `static_assert(SingleKeyingResult::SUCCESS == 0)` could be a nice touch here.

decided to do that right away, imo this makes it more obvious what the code is expecting

decided to do that right away, imo this makes it more obvious what the code is expecting
for (int i = 1; i < result_counter.size(); i++) {
if (result_counter[i] > 0) {
return true;
}
return false;
}
};
return false;
}

Below this code is called when get_count(animrig::SingleKeyingResult::SUCCESS) == 0. However, this function does not take that case into account. The success count could theoretically be zero because nothing happened, in which case the generated report will end with "due to the following reasons:", which will be rather strange.

It's probably best to have a bool has_reported_error that's set to true in each specific if (error count > 0) block. That way you can check at the bottom of the function and, if it's false, add a report that explains the situation. That will ensure that something is shown, even when a configuration occurs that isn't (yet) supported by this function.

Below this code is called when `get_count(animrig::SingleKeyingResult::SUCCESS) == 0`. However, this function does not take that case into account. The success count could theoretically be zero because nothing happened, in which case the generated report will end with "due to the following reasons:", which will be rather strange. It's probably best to have a `bool has_reported_error` that's set to `true` in each specific `if (error count > 0)` block. That way you can check at the bottom of the function and, if it's `false`, add a report that explains the situation. That will ensure that something is shown, even when a configuration occurs that isn't (yet) supported by this function.
void CombinedKeyingResult::generate_reports(ReportList *reports)
{
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::CANNOT_CREATE_FCURVE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE);
errors.append(
fmt::format("Could not create {} F-Curve{}. This can happen when only inserting to "
"available F-Curves.",
error_count,
error_count > 1 ? "s" : ""));
}
if (this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE) > 0) {
const int error_count = this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE);
if (error_count == 1) {
errors.append("One F-Curve is not keyframeable. It might be locked or sampled.");
}
else {
errors.append(fmt::format(
"{} F-Curves 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("Due to the setting 'Only Insert Needed', {} keyframe{} not been inserted.",
error_count,
error_count > 1 ? "s have" : " has"));
}
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("Due to the NLA stack setup, {} keyframe{} not been inserted.",
error_count,
error_count > 1 ? "s have" : " has"));
}
if (errors.is_empty()) {
BKE_report(reports, RPT_WARNING, "Encountered unhandled error during keyframing");
return;
}
if (errors.size() == 1) {
BKE_report(reports, RPT_ERROR, errors[0].c_str());
return;

BKE_report(reports, RPT_ERROR, error.c_str());

`BKE_report(reports, RPT_ERROR, error.c_str());`
}
std::string error_message = "Inserting keyframes failed:";
for (const std::string &error : errors) {
error_message.append(fmt::format("\n- {}", error));
}
BKE_report(reports, RPT_ERROR, error_message.c_str());
}
void update_autoflags_fcurve_direct(FCurve *fcu, PropertyRNA *prop)
{
@ -499,42 +557,6 @@ static SingleKeyingResult insert_keyframe_fcurve_value(Main *bmain,
return result;
}
static void generate_keyframe_reports_from_result(ReportList *reports,
const CombinedKeyingResult &result)
{
std::string error = "Inserting keyframes failed due to the following reasons:";
if (result.get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE) > 0) {
const int error_count = result.get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE);
error.append(
fmt::format("\n- Could not create {} F-Curve{}. This can happen when only inserting to "
"available F-Curves.",
error_count,
error_count > 1 ? "s" : ""));
}
if (result.get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE) > 0) {
const int error_count = result.get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE);
if (error_count == 1) {
error.append("\n- One F-Curve is not keyframeable. It might be locked or sampled.");
}
else {
error.append(fmt::format(
"\n- {} F-Curves are not keyframeable. They might be locked or sampled.", error_count));
}
}
if (result.get_count(SingleKeyingResult::NO_KEY_NEEDED) > 0) {
const int error_count = result.get_count(SingleKeyingResult::NO_KEY_NEEDED);
error.append(fmt::format(
"\n- Due to the setting 'Only Insert Needed', {} keyframe{} not been inserted.",
error_count,
error_count > 1 ? "s have" : " has"));
}
BKE_reportf(reports, RPT_ERROR, "%s", error.c_str());
}
int insert_keyframe(Main *bmain,
ReportList *reports,
ID *id,
@ -718,7 +740,7 @@ int insert_keyframe(Main *bmain,
}
if (key_count == 0) {
generate_keyframe_reports_from_result(reports, combined_result);
combined_result.generate_reports(reports);
}
return key_count;
@ -916,16 +938,16 @@ int clear_keyframe(Main *bmain,
return key_count;
}
int insert_key_action(Main *bmain,
bAction *action,
PointerRNA *ptr,
PropertyRNA *prop,
const std::string &rna_path,
const float frame,
const Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type,
const BitSpan keying_mask)
CombinedKeyingResult insert_key_action(Main *bmain,
bAction *action,
PointerRNA *ptr,
PropertyRNA *prop,
const std::string &rna_path,
const float frame,
const Span<float> values,
eInsertKeyFlags insert_key_flag,
eBezTriple_KeyframeType key_type,
const BitSpan keying_mask)
{
BLI_assert(bmain != nullptr);
BLI_assert(action != nullptr);
@ -940,49 +962,44 @@ int insert_key_action(Main *bmain,
}
int property_array_index = 0;
int inserted_keys = 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 SingleKeyingResult inserted_key = insert_keyframe_fcurve_value(bmain,
ptr,
prop,
action,
group.c_str(),
rna_path.c_str(),
property_array_index,
frame,
value,
key_type,
insert_key_flag);
if (inserted_key == SingleKeyingResult::SUCCESS) {
inserted_keys++;
}
const SingleKeyingResult keying_result = insert_keyframe_fcurve_value(bmain,
ptr,
prop,
action,
group.c_str(),
rna_path.c_str(),
property_array_index,
frame,
value,
key_type,
insert_key_flag);
combined_result.add(keying_result);
property_array_index++;
}
return inserted_keys;
return combined_result;
}
void insert_key_rna(PointerRNA *rna_pointer,
const blender::Span<std::string> rna_paths,
const float scene_frame,
const eInsertKeyFlags insert_key_flags,
const eBezTriple_KeyframeType key_type,
Main *bmain,
ReportList *reports,
const AnimationEvalContext &anim_eval_context)
CombinedKeyingResult insert_key_rna(PointerRNA *rna_pointer,
const blender::Span<std::string> rna_paths,
const float scene_frame,
const eInsertKeyFlags insert_key_flags,
const eBezTriple_KeyframeType key_type,
Main *bmain,
const AnimationEvalContext &anim_eval_context)
{
ID *id = rna_pointer->owner_id;
bAction *action = id_action_ensure(bmain, id);
CombinedKeyingResult combined_result;
if (action == nullptr) {
BKE_reportf(reports,
RPT_ERROR,
"Could not insert keyframe, as this type does not support animation data (ID = "
"%s)",
id->name);
return;
return combined_result;
}
AnimData *adt = BKE_animdata_from_id(id);
@ -1000,19 +1017,12 @@ void insert_key_rna(PointerRNA *rna_pointer,
const float nla_frame = BKE_nla_tweakedit_remap(adt, scene_frame, NLATIME_CONVERT_UNMAP);
const bool visual_keyframing = insert_key_flags & INSERTKEY_MATRIX;
int insert_key_count = 0;
for (const std::string &rna_path : rna_paths) {
PointerRNA ptr;
PropertyRNA *prop = nullptr;
const bool path_resolved = RNA_path_resolve_property(
rna_pointer, rna_path.c_str(), &ptr, &prop);
if (!path_resolved) {
BKE_reportf(reports,
RPT_ERROR,
"Could not insert keyframe, as this property does not exist (ID = "
"%s, path = %s)",
id->name,
rna_path.c_str());
continue;
}
const std::optional<std::string> rna_path_id_to_prop = RNA_path_from_ID_to_property(&ptr,
@ -1028,23 +1038,21 @@ void insert_key_rna(PointerRNA *rna_pointer,
&anim_eval_context,
nullptr,
successful_remaps);
insert_key_count += insert_key_action(bmain,
action,
rna_pointer,
prop,
rna_path_id_to_prop->c_str(),
nla_frame,
rna_values.as_span(),
insert_key_flags,
key_type,
successful_remaps);
const CombinedKeyingResult result = insert_key_action(bmain,
action,
rna_pointer,
prop,
rna_path_id_to_prop->c_str(),
nla_frame,
rna_values.as_span(),
insert_key_flags,
key_type,
successful_remaps);
combined_result.merge(result);
}
BKE_animsys_free_nla_keyframing_context_cache(&nla_cache);
if (insert_key_count == 0) {
BKE_reportf(reports, RPT_ERROR, "Failed to insert any keys");
}
return combined_result;
}
} // namespace blender::animrig

View File

@ -159,7 +159,6 @@ void autokeyframe_object(bContext *C, Scene *scene, Object *ob, Span<std::string
flag,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
bmain,
reports,
anim_eval_context);
}
}
@ -284,7 +283,6 @@ void autokeyframe_pose_channel(bContext *C,
flag,
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
bmain,
reports,
anim_eval_context);
}
}

View File

@ -336,6 +336,12 @@ static int insert_key(bContext *C, wmOperator *op)
const bool found_selection = get_selection(C, &selection);
if (!found_selection) {
BKE_reportf(op->reports, RPT_ERROR, "Unsupported context mode");
return OPERATOR_CANCELLED;
}
if (selection.is_empty()) {
BKE_reportf(op->reports, RPT_WARNING, "Nothing selected to key");
return OPERATOR_CANCELLED;

In the rest of this PR "keyframe" is only used as a noun. Maybe "Nothing selected to key" is better?

In the rest of this PR "keyframe" is only used as a noun. Maybe "Nothing selected to key" is better?
}
Main *bmain = CTX_data_main(C);
@ -349,22 +355,34 @@ static int insert_key(bContext *C, wmOperator *op)
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(
depsgraph, BKE_scene_frame_get(scene));
animrig::CombinedKeyingResult combined_result;
for (PointerRNA &id_ptr : selection) {
ID *selected_id = id_ptr.owner_id;
if (!id_can_have_animdata(selected_id)) {
BKE_reportf(op->reports,
RPT_ERROR,
"Could not insert keyframe, as this type does not support animation data (ID = "
"%s)",
selected_id->name);
continue;
}
if (!BKE_id_is_editable(bmain, selected_id)) {
BKE_reportf(op->reports, RPT_ERROR, "'%s' is not editable", selected_id->name + 2);
continue;
}
Vector<std::string> rna_paths = construct_rna_paths(&id_ptr);
animrig::insert_key_rna(&id_ptr,
rna_paths.as_span(),
scene_frame,
insert_key_flags,
key_type,
bmain,
op->reports,
anim_eval_context);
combined_result.merge(animrig::insert_key_rna(&id_ptr,
rna_paths.as_span(),
scene_frame,
insert_key_flags,
key_type,
bmain,

This is a bit of a weird thing to check for, given that there is a function has_errors(). I can imagine it's necessary, as there are cases in the code above where a call to insert_key_rna() is skipped, and thus it wouldn't register as error on the combined_result.

I think the weirdness comes from the use of the mechanism to track key insertion errors to track key insertion skips. I think it's better to just use combined_result.has_errors() here, as the skips already have their own calls to BKE_report.

This is a bit of a weird thing to check for, given that there is a function `has_errors()`. I can imagine it's necessary, as there are cases in the code above where a call to `insert_key_rna()` is skipped, and thus it wouldn't register as error on the `combined_result`. I think the weirdness comes from the use of the mechanism to track key insertion **errors** to track key insertion **skips**. I think it's better to just use `combined_result.has_errors()` here, as the skips already have their own calls to `BKE_report`.

using has_errors() is not the same as checking if there was at least one success.
The reason I am checking for 0 successes is that we don't want to spam the user with messages. E.g. there might be an FCurve that is locked and the user is aware of that, still when we use has_errors he would get an error popup every single time he wants to insert a key.

Edit:
what I did now is to explicitly check for an empty selection list and report a warning + exit early. That makes it clearer what happened.

using `has_errors()` is not the same as checking if there was at least one success. The reason I am checking for 0 successes is that we don't want to spam the user with messages. E.g. there might be an FCurve that is locked and the user is aware of that, still when we use `has_errors` he would get an error popup every single time he wants to insert a key. Edit: what I did now is to explicitly check for an empty selection list and report a warning + exit early. That makes it clearer what happened.
anim_eval_context));
}
if (combined_result.get_count(animrig::SingleKeyingResult::SUCCESS) == 0) {
combined_result.generate_reports(op->reports);
}
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr);