Anim: Detailed report if no keyframes have been inserted #119201
|
@ -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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
Sybren A. Stüvel
commented
For a future improvement: adding a For a future improvement: adding a `static_assert(SingleKeyingResult::SUCCESS == 0)` could be a nice touch here.
|
||||
for (int i = 1; i < result_counter.size(); i++) {
|
||||
if (result_counter[i] > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
Sybren A. Stüvel
commented
Below this code is called when It's probably best to have a 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;
|
||||
Sybren A. Stüvel
commented
`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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
Sybren A. Stüvel
commented
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,
|
||||
Sybren A. Stüvel
commented
This is a bit of a weird thing to check for, given that there is a function 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 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 Edit: 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);
|
||||
|
|
How often keyframe insertion was successful
and
how often it failed/
for which reason.Same for the copies below, of course.