Anim: add low-level function for simple FCurve key deduplication #107089

Manually merged
Sybren A. Stüvel merged 3 commits from dr.sybren/blender:anim/deduplicate-keys into main 2023-04-24 12:34:28 +02:00
7 changed files with 331 additions and 18 deletions

View File

@ -571,4 +571,8 @@ class KeyframesCo:
keyframe_points.foreach_set("co", co_buffer)
keyframe_points.foreach_set("interpolation", ipo_buffer)
fcurve.update()
# TODO: in Blender 4.0 the next lines can be replaced with one call to `fcurve.update()`.
# See https://projects.blender.org/blender/blender/issues/107126 for more info.
keyframe_points.sort()
dr.sybren marked this conversation as resolved Outdated

update() should not be there. I understand that it'll be combined in 4.0 but this patch is about performance. For mocap or animation exporting, we'd likely be baking hundreds of frames and thousands of keys.

update() should not be there. I understand that it'll be combined in 4.0 but this patch is about performance. For mocap or animation exporting, we'd likely be baking hundreds of frames and thousands of keys.

Unless I've misunderstood something (which is always possible), I believe update() is needed here because it sorts the keyframes, which is a necessary precondition for deduplicate() to function correctly.

Unless I've misunderstood something (which is always possible), I believe `update()` is needed here because it sorts the keyframes, which is a necessary precondition for `deduplicate()` to function correctly.

You've got it right. I should've said to add keyframe.sort() too. Update should still not be there.

You've got it right. I should've said to add keyframe.sort() too. Update should still not be there.

Ah, yeah, that makes sense. It looks like update() is also responsible for recalculating handles and tagging for animation update, which I assume need to happen? But those should probably be after deduplicating anyway.

Ah, yeah, that makes sense. It looks like `update()` is also responsible for recalculating handles and tagging for animation update, which I assume need to happen? But those should probably be after deduplicating anyway.

It'll still need recalculating the handles so I'll remove .update() and replace it with sort+dedup+handles. That way the recalculation of the handles is only done once.

It'll still need recalculating the handles so I'll remove `.update()` and replace it with sort+dedup+handles. That way the recalculation of the handles is only done once.

It'll still need recalculating the handles so I'll remove .update() and replace it with sort+dedup+handles.

Did you make this change already? I don't see it on Gitea. Or is this a change for 4.0?

> It'll still need recalculating the handles so I'll remove .update() and replace it with sort+dedup+handles. Did you make this change already? I don't see it on Gitea. Or is this a change for 4.0?

Huh, that's weird. As soon as I made this reply, it changed for me and showed that you did make the change. I guess Gitea is just being weird, or I misunderstood something about how it works.

Huh, that's weird. As soon as I made this reply, it changed for me and showed that you did make the change. I guess Gitea is just being weird, or I misunderstood something about how it works.
keyframe_points.deduplicate()
keyframe_points.handles_recalc()

View File

@ -416,6 +416,7 @@ int BKE_fcurve_active_keyframe_index(const struct FCurve *fcu);
* Move the indexed keyframe to the given value,
* and move the handles with it to ensure the slope remains the same.
*/
void BKE_fcurve_keyframe_move_time_with_handles(BezTriple *keyframe, const float new_time);
void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, float new_value);
/* .............. */
@ -472,6 +473,14 @@ bool BKE_fcurve_bezt_subdivide_handles(struct BezTriple *bezt,
struct BezTriple *next,
float *r_pdelta);
/**
* Resize the FCurve 'bezt' array to fit the given length.
*
* \param new_totvert new number of elements in the FCurve's `bezt` array.
* Constraint: `0 <= new_totvert <= fcu->totvert`
*/
void BKE_fcurve_bezt_shrink(struct FCurve *fcu, int new_totvert);
/**
* Delete a keyframe from an F-curve at a specific index.
*/
@ -499,6 +508,21 @@ void BKE_fcurve_merge_duplicate_keys(struct FCurve *fcu,
const int sel_flag,
const bool use_handle);
/**
* Ensure the FCurve is a proper function, such that every X-coordinate of the
* timeline has only one value of the FCurve. In other words, removes duplicate
* keyframes.
*
* Contrary to #BKE_fcurve_merge_duplicate_keys, which is intended for
* interactive use, and where selection matters, this is a simpler deduplication
* where the last duplicate "wins".
*
* Assumes the keys are sorted (see #sort_time_fcurve).
*
* After deduplication, call `BKE_fcurve_handles_recalc(fcu);`
*/
void BKE_fcurve_deduplicate_keys(struct FCurve *fcu);
/* -------- Curve Sanity -------- */
/**

View File

@ -883,6 +883,14 @@ int BKE_fcurve_active_keyframe_index(const FCurve *fcu)
/** \} */
void BKE_fcurve_keyframe_move_time_with_handles(BezTriple *keyframe, const float new_time)
{
const float time_delta = new_time - keyframe->vec[1][0];
keyframe->vec[0][0] += time_delta;
keyframe->vec[1][0] = new_time;
keyframe->vec[2][0] += time_delta;
}
void BKE_fcurve_keyframe_move_value_with_handles(struct BezTriple *keyframe, const float new_value)
{
const float value_delta = new_value - keyframe->vec[1][1];
@ -1659,6 +1667,24 @@ bool BKE_fcurve_bezt_subdivide_handles(struct BezTriple *bezt,
return true;
}
void BKE_fcurve_bezt_shrink(struct FCurve *fcu, const int new_totvert)
{
BLI_assert(new_totvert >= 0);
BLI_assert(new_totvert <= fcu->totvert);
/* No early return when new_totvert == fcu->totvert. There is no way to know the intention of the
* caller, nor the history of the FCurve so far, so `fcu->bezt` may actually have allocated space
* for more than `fcu->totvert` keys. */
if (new_totvert == 0) {
fcurve_bezt_free(fcu);
return;
}
fcu->bezt = MEM_reallocN(fcu->bezt, new_totvert * sizeof(*(fcu->bezt)));
fcu->totvert = new_totvert;
}
void BKE_fcurve_delete_key(FCurve *fcu, int index)
{
/* sanity check */
@ -1849,6 +1875,52 @@ void BKE_fcurve_merge_duplicate_keys(FCurve *fcu, const int sel_flag, const bool
BLI_freelistN(&retained_keys);
}
void BKE_fcurve_deduplicate_keys(FCurve *fcu)
{
BLI_assert_msg(fcu->bezt, "this function only works with regular (non-sampled) FCurves");
if (fcu->totvert < 2 || fcu->bezt == NULL) {
return;
}
int prev_bezt_index = 0;
for (int i = 1; i < fcu->totvert; i++) {
BezTriple *bezt = &fcu->bezt[i];

Don't modify prev_bezt's time. (Edit: This would result in time-shifting of 0.01 but, atm, I don't think there's a better solution. Even if we stop comparing with a running prev_x and instead use the prev_x of the first replaced one, then that still allows two keys to be within the threshold for a follow-up deduplicate() call and we end up with a variation of the problem below)

If (bezt_x > prev_x) and (bezt_x != prev_x), then we're replacing prev_x with bezt_x and repeatedly doing so since bezt_x might be very close to the next one and so on. If, for whatever reason, the user has scaled their keys by 1/100 (BEZT_BINARYSEARCH_THRESH=0.01) and they had keys on every frame, then we're effectively deleting all keys except for the last one.

A problematic practical case (still a bit contrived) I can think of is a cyclic fcurve that has keyframe-implemented noise or oscillation (camera shake, hair wind simulation, etc) so time-scaling the curve changes the frequency.

Don't modify prev_bezt's time. (Edit: This would result in time-shifting of 0.01 but, atm, I don't think there's a better solution. Even if we stop comparing with a running prev_x and instead use the prev_x of the first replaced one, then that still allows two keys to be within the threshold for a follow-up deduplicate() call and we end up with a variation of the problem below) If (bezt_x > prev_x) and (bezt_x != prev_x), then we're replacing prev_x with bezt_x and repeatedly doing so since bezt_x might be very close to the next one and so on. If, for whatever reason, the user has scaled their keys by 1/100 (BEZT_BINARYSEARCH_THRESH=0.01) and they had keys on every frame, then we're effectively deleting all keys except for the last one. A problematic practical case (still a bit contrived) I can think of is a cyclic fcurve that has keyframe-implemented noise or oscillation (camera shake, hair wind simulation, etc) so time-scaling the curve changes the frequency.
BezTriple *prev_bezt = &fcu->bezt[prev_bezt_index];
const float bezt_x = bezt->vec[1][0];
const float prev_x = prev_bezt->vec[1][0];
if (bezt_x - prev_x <= BEZT_BINARYSEARCH_THRESH) {
/* Replace 'prev_bezt', as it has the same X-coord as 'bezt' and the last one wins. */
*prev_bezt = *bezt;
if (floor(bezt_x) == bezt_x) {
/* Keep the 'bezt_x' coordinate, as being on a frame is more desirable
* than being ever so slightly off. */
}
else {
/* Move the retained key to the old X-coordinate, to 'anchor' the X-coordinate used for
* subsequente comparisons. Without this, the reference X-coordinate would keep moving
* forward in time, potentially merging in more keys than desired. */
BKE_fcurve_keyframe_move_time_with_handles(prev_bezt, prev_x);
}
continue;
}
/* Next iteration should look at the current element. However, because of the deletions, that
* may not be at index 'i'; after this increment, `prev_bezt_index` points at where the current
* element should go. */
prev_bezt_index++;
if (prev_bezt_index != i) {
/* This bezt should be kept, so copy it to its new location in the array. */
fcu->bezt[prev_bezt_index] = *bezt;
}
}
BKE_fcurve_bezt_shrink(fcu, prev_bezt_index + 1);
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -10,6 +10,8 @@
#include "DNA_anim_types.h"
#include "BLI_math_vector_types.hh"
namespace blender::bke::tests {
/* Epsilon for floating point comparisons. */
@ -346,6 +348,37 @@ TEST(BKE_fcurve, BKE_fcurve_keyframe_move_value_with_handles)
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_keyframe_move_time_with_handles)
{
FCurve *fcu = BKE_fcurve_create();
insert_vert_fcurve(fcu, 1.0f, 7.5f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
insert_vert_fcurve(fcu, 8.0f, 15.0f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
insert_vert_fcurve(fcu, 14.0f, 8.2f, BEZT_KEYTYPE_KEYFRAME, INSERTKEY_NO_USERPREF);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][0], 5.2671194f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][1], 15.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][0], 8.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][1], 15.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][0], 10.342469f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][1], 15.0f);
BKE_fcurve_keyframe_move_time_with_handles(&fcu->bezt[1], 47.0f);
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][0], 44.2671194f) << "Left handle time should be updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[0][1], 15.0f) << "Left handle should not move in value";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][0], 47.0f) << "Frame time should have been updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[1][1], 15.0f) << "Frame should not move in value";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][0], 49.342469f) << "Right handle time should be updated";
EXPECT_FLOAT_EQ(fcu->bezt[1].vec[2][1], 15.0f) << "Right handle should not move in value";
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_calc_range)
{
FCurve *fcu = BKE_fcurve_create();
@ -546,4 +579,127 @@ TEST(BKE_fcurve, BKE_fcurve_calc_bounds)
BKE_fcurve_free(fcu);
}
static void set_key(FCurve *fcu, const int index, const float x, const float y)
{
fcu->bezt[index].vec[0][0] = x - 0.5f;
fcu->bezt[index].vec[1][0] = x;
fcu->bezt[index].vec[2][0] = x + 0.5f;
fcu->bezt[index].vec[0][1] = y;
fcu->bezt[index].vec[1][1] = y;
fcu->bezt[index].vec[2][1] = y;
}
static FCurve *testcurve_with_duplicates()
{
/* Create a curve with some duplicate keys. The first ones are all with Y=1, the later repeats
* increase Y-coordinates on every repeat. */
FCurve *fcu = BKE_fcurve_create();
ED_keyframes_add(fcu, 10); /* Avoid `insert_vert_fcurve`, that deduplicates the keys. */
set_key(fcu, 0, 1.0f, 1.0f);
set_key(fcu, 1, 327.16f, 1.0f);
set_key(fcu, 2, 7.0f, 1.0f);
set_key(fcu, 3, 47.0f, 1.0f);
set_key(fcu, 4, 7.0f, 2.0f);
set_key(fcu, 5, 47.0f, 2.0f);
set_key(fcu, 6, 47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f);
set_key(fcu, 7, 7.0f, 3.0f);
set_key(fcu, 8, 3.0f, 1.0f);
set_key(fcu, 9, 2.0f, 1.0f);
return fcu;
}
TEST(BKE_fcurve, sort_time_fcurve_stability)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
sort_time_fcurve(fcu);
/* The sorting should be stable, i.e. retain the original order when the
* X-coordinates are identical. */
ASSERT_EQ(fcu->totvert, 10) << "sorting should not influence number of keys";
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(7.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[6].vec[1], float2(47.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[7].vec[1], float2(47.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[8].vec[1], float2(47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[9].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_GE(fcu->totvert, 6); /* Protect against out-of-bounds access. */
EXPECT_EQ(fcu->totvert, 6); /* The actual expected value. */
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys_edge_cases)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
/* Update the 2nd and 2nd-to-last keys to test the edge cases. */
set_key(fcu, 0, 1, 1);
set_key(fcu, 1, 1, 2);
set_key(fcu, 8, 327.16f, 1);
set_key(fcu, 9, 327.16f, 2);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_EQ(fcu->totvert, 4);
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 2.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(327.16f, 2.0f), 1e-3);
BKE_fcurve_free(fcu);
}
TEST(BKE_fcurve, BKE_fcurve_deduplicate_keys_prefer_whole_frames)
{
FCurve *fcu = testcurve_with_duplicates();
ASSERT_EQ(fcu->totvert, 10);
/* Update the first key around 47.0 to be slightly before the frame. This gives us three keys on
* 47-epsilon, 47, and 47+epsilon. The keys at index 5 and 6 already have this value, so the
* `set_key` calls are unnecessary, but this way this test has a more local overview of the
* situation under test. */
set_key(fcu, 3, 47.0f - BEZT_BINARYSEARCH_THRESH, 1.0f);
set_key(fcu, 5, 47.0f, 2.0f);
set_key(fcu, 6, 47.0f + BEZT_BINARYSEARCH_THRESH, 3.0f);
sort_time_fcurve(fcu);
BKE_fcurve_deduplicate_keys(fcu);
ASSERT_EQ(fcu->totvert, 6);
EXPECT_V2_NEAR(fcu->bezt[0].vec[1], float2(1.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[1].vec[1], float2(2.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[2].vec[1], float2(3.0f, 1.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[3].vec[1], float2(7.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[4].vec[1], float2(47.0f, 3.0f), 1e-3);
EXPECT_V2_NEAR(fcu->bezt[5].vec[1], float2(327.16f, 1.0f), 1e-3);
BKE_fcurve_free(fcu);
}
} // namespace blender::bke::tests

View File

@ -1659,6 +1659,29 @@ int insert_keyframe(Main *bmain,
return ret;
}
void ED_keyframes_add(FCurve *fcu, int num_keys_to_add)
{
BLI_assert_msg(num_keys_to_add >= 0, "cannot remove keyframes with this function");
if (num_keys_to_add == 0) {
return;
}
fcu->bezt = MEM_recallocN(fcu->bezt, sizeof(BezTriple) * (fcu->totvert + num_keys_to_add));
BezTriple *bezt = fcu->bezt + fcu->totvert; /* Pointer to the first new one. '*/
fcu->totvert += num_keys_to_add;
/* Iterate over the new keys to update their settings. */
while (num_keys_to_add--) {
/* Defaults, ignoring user-preference gives predictable results for API. */
bezt->f1 = bezt->f2 = bezt->f3 = SELECT;
bezt->ipo = BEZT_IPO_BEZ;
bezt->h1 = bezt->h2 = HD_AUTO_ANIM;
bezt++;
}
}
/* ************************************************** */
/* KEYFRAME DELETION */

View File

@ -123,6 +123,17 @@ int insert_vert_fcurve(struct FCurve *fcu,
eBezTriple_KeyframeType keyframe_type,
eInsertKeyFlags flag);
/**
* Add the given number of keyframes to the FCurve. Their coordinates are
* uninitialized, so the curve should not be used without further attention.
*
* The newly created keys are selected, existing keys are not touched.
*
* This can be used to allocate all the keys at once, and then update them
* afterwards.
*/
void ED_keyframes_add(struct FCurve *fcu, int num_keys_to_add);
/* -------- */
/**

View File

@ -692,6 +692,7 @@ static void rna_tag_animation_update(Main *bmain, ID *id)
static void rna_FCurve_update_data_ex(ID *id, FCurve *fcu, Main *bmain)
{
sort_time_fcurve(fcu);
/* TODO: Blender 4.0 should call BKE_fcurve_deduplicate_keys(fcu) here. */
BKE_fcurve_handles_recalc(fcu);
rna_tag_animation_update(bmain, id);
@ -1071,24 +1072,12 @@ static BezTriple *rna_FKeyframe_points_insert(
static void rna_FKeyframe_points_add(ID *id, FCurve *fcu, Main *bmain, int tot)
{
if (tot > 0) {
BezTriple *bezt;
fcu->bezt = MEM_recallocN(fcu->bezt, sizeof(BezTriple) * (fcu->totvert + tot));
bezt = fcu->bezt + fcu->totvert;
fcu->totvert += tot;
while (tot--) {
/* Defaults, ignoring user-preference gives predictable results for API. */
bezt->f1 = bezt->f2 = bezt->f3 = SELECT;
bezt->ipo = BEZT_IPO_BEZ;
bezt->h1 = bezt->h2 = HD_AUTO_ANIM;
bezt++;
}
rna_tag_animation_update(bmain, id);
if (tot <= 0) {
return;
}
ED_keyframes_add(fcu, tot);
rna_tag_animation_update(bmain, id);
}
static void rna_FKeyframe_points_remove(
@ -1118,6 +1107,24 @@ static void rna_FKeyframe_points_clear(ID *id, FCurve *fcu, Main *bmain)
rna_tag_animation_update(bmain, id);
}
static void rna_FKeyframe_points_sort(ID *id, FCurve *fcu, Main *bmain)
{
sort_time_fcurve(fcu);
rna_tag_animation_update(bmain, id);
dr.sybren marked this conversation as resolved Outdated

handles_recalc() should not be added in the same function. The same goes for deduplicate(). User-wise, the proper way to use these functions would be: sort() then deduplicate() which recalculates all of the handles twice.

handles_recalc() should not be added in the same function. The same goes for deduplicate(). User-wise, the proper way to use these functions would be: sort() then deduplicate() which recalculates all of the handles twice.

You're absolutely right, you can see I was rushing to get the patch uploaded before running to a concert by a friend :)

You're absolutely right, you can see I was rushing to get the patch uploaded before running to a concert by a friend :)
}
static void rna_FKeyframe_points_deduplicate(ID *id, FCurve *fcu, Main *bmain)
{
BKE_fcurve_deduplicate_keys(fcu);
rna_tag_animation_update(bmain, id);
}
static void rna_FKeyframe_points_handles_recalc(ID *id, FCurve *fcu, Main *bmain)
{
BKE_fcurve_handles_recalc(fcu);
rna_tag_animation_update(bmain, id);
}
static FCM_EnvelopeData *rna_FModifierEnvelope_points_add(
ID *id, FModifier *fmod, Main *bmain, ReportList *reports, float frame)
{
@ -2414,6 +2421,22 @@ static void rna_def_fcurve_keyframe_points(BlenderRNA *brna, PropertyRNA *cprop)
func = RNA_def_function(srna, "clear", "rna_FKeyframe_points_clear");
RNA_def_function_ui_description(func, "Remove all keyframes from an F-Curve");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "sort", "rna_FKeyframe_points_sort");
RNA_def_function_ui_description(func, "Ensure all keyframe points are chronologically sorted");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "deduplicate", "rna_FKeyframe_points_deduplicate");
RNA_def_function_ui_description(
func,
"Ensure there are no duplicate keys. Assumes that the points have already been sorted");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
func = RNA_def_function(srna, "handles_recalc", "rna_FKeyframe_points_handles_recalc");
RNA_def_function_ui_description(func,
"Update handles after modifications to the keyframe points, to "
"update things like auto-clamping");
RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN);
}
static void rna_def_fcurve(BlenderRNA *brna)