/* SPDX-FileCopyrightText: 2008 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup edanimation */ #include #include #include #include #include #include #include "MEM_guardedalloc.h" #include "BLI_listbase.h" #include "BLI_math_vector.h" #include "BLI_string.h" #include "BLI_string_utils.hh" #include "BLI_utildefines.h" #include "DNA_anim_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "DNA_space_types.h" #include "BKE_action.hh" #include "BKE_curve.hh" #include "BKE_fcurve.hh" #include "BKE_main.hh" #include "BKE_scene.hh" #include "RNA_access.hh" #include "RNA_enum_types.hh" #include "RNA_path.hh" #include "ED_keyframes_edit.hh" #include "ANIM_action.hh" #include "ANIM_animdata.hh" #include "ANIM_fcurve.hh" #include "keyframes_general_intern.hh" /* This file contains code for various keyframe-editing tools which are 'destructive' * (i.e. they will modify the order of the keyframes, and change the size of the array). * While some of these tools may eventually be moved out into blenkernel, for now, it is * fine to have these calls here. * * There are also a few tools here which cannot be easily coded for in the other system (yet). * These may also be moved around at some point, but for now, they are best added here. * * - Joshua Leung, Dec 2008 */ /* **************************************************** */ bool duplicate_fcurve_keys(FCurve *fcu) { bool changed = false; /* this can only work when there is an F-Curve, and also when there are some BezTriples */ if (ELEM(nullptr, fcu, fcu->bezt)) { return changed; } for (int i = 0; i < fcu->totvert; i++) { /* If a key is selected */ if (fcu->bezt[i].f2 & SELECT) { /* Expand the list */ BezTriple *newbezt = static_cast( MEM_callocN(sizeof(BezTriple) * (fcu->totvert + 1), "beztriple")); memcpy(newbezt, fcu->bezt, sizeof(BezTriple) * (i + 1)); memcpy(newbezt + i + 1, fcu->bezt + i, sizeof(BezTriple)); memcpy(newbezt + i + 2, fcu->bezt + i + 1, sizeof(BezTriple) * (fcu->totvert - (i + 1))); fcu->totvert++; changed = true; /* reassign pointers... (free old, and add new) */ MEM_freeN(fcu->bezt); fcu->bezt = newbezt; /* Unselect the current key */ BEZT_DESEL_ALL(&fcu->bezt[i]); i++; /* Select the copied key */ BEZT_SEL_ALL(&fcu->bezt[i]); } } return changed; } /* -------------------------------------------------------------------- */ /** \name Various Tools * \{ */ void clean_fcurve(bAnimListElem *ale, float thresh, bool cleardefault, const bool only_selected_keys) { FCurve *fcu = static_cast(ale->key_data); BezTriple *old_bezts, *bezt, *beztn; BezTriple *lastb; int totCount, i; /* Check if any points. */ if ((fcu == nullptr) || (fcu->bezt == nullptr) || (fcu->totvert == 0) || (!cleardefault && fcu->totvert == 1)) { return; } /* make a copy of the old BezTriples, and clear F-Curve */ old_bezts = fcu->bezt; totCount = fcu->totvert; fcu->bezt = nullptr; fcu->totvert = 0; /* now insert first keyframe, as it should be ok */ bezt = old_bezts; blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); if (!(bezt->f2 & SELECT)) { lastb = fcu->bezt; lastb->f1 = lastb->f2 = lastb->f3 = 0; } /* Loop through BezTriples, comparing them. Skip any that do * not fit the criteria for "ok" points. */ for (i = 1; i < totCount; i++) { float prev[2], cur[2], next[2]; /* get BezTriples and their values */ if (i < (totCount - 1)) { beztn = (old_bezts + (i + 1)); next[0] = beztn->vec[1][0]; next[1] = beztn->vec[1][1]; } else { beztn = nullptr; next[0] = next[1] = 0.0f; } lastb = (fcu->bezt + (fcu->totvert - 1)); bezt = (old_bezts + i); /* get references for quicker access */ prev[0] = lastb->vec[1][0]; prev[1] = lastb->vec[1][1]; cur[0] = bezt->vec[1][0]; cur[1] = bezt->vec[1][1]; if (only_selected_keys && !(bezt->f2 & SELECT)) { blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); lastb = (fcu->bezt + (fcu->totvert - 1)); lastb->f1 = lastb->f2 = lastb->f3 = 0; continue; } /* check if current bezt occurs at same time as last ok */ if (IS_EQT(cur[0], prev[0], thresh)) { /* If there is a next beztriple, and if occurs at the same time, only insert * if there is a considerable distance between the points, and also if the * current is further away than the next one is to the previous. */ if (beztn && IS_EQT(cur[0], next[0], thresh) && (IS_EQT(next[1], prev[1], thresh) == 0)) { /* only add if current is further away from previous */ if (cur[1] > next[1]) { if (IS_EQT(cur[1], prev[1], thresh) == 0) { /* add new keyframe */ blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } } } else { /* only add if values are a considerable distance apart */ if (IS_EQT(cur[1], prev[1], thresh) == 0) { /* add new keyframe */ blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } } } else { /* checks required are dependent on whether this is last keyframe or not */ if (beztn) { /* does current have same value as previous and next? */ if (IS_EQT(cur[1], prev[1], thresh) == 0) { /* add new keyframe */ blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } else if (IS_EQT(cur[1], next[1], thresh) == 0) { /* add new keyframe */ blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } } else { /* add if value doesn't equal that of previous */ if (IS_EQT(cur[1], prev[1], thresh) == 0) { /* add new keyframe */ blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } } } } /* now free the memory used by the old BezTriples */ if (old_bezts) { MEM_freeN(old_bezts); } /* final step, if there is just one key in fcurve, check if it's * the default value and if is, remove fcurve completely. */ if (cleardefault && fcu->totvert == 1) { float default_value = 0.0f; PointerRNA ptr; PropertyRNA *prop; PointerRNA id_ptr = RNA_id_pointer_create(ale->id); /* get property to read from, and get value as appropriate */ if (RNA_path_resolve_property(&id_ptr, fcu->rna_path, &ptr, &prop)) { if (RNA_property_type(prop) == PROP_FLOAT) { default_value = RNA_property_float_get_default_index(&ptr, prop, fcu->array_index); } } if (fcu->bezt->vec[1][1] == default_value) { BKE_fcurve_delete_keys_all(fcu); /* check if curve is really unused and if it is, return signal for deletion */ if (BKE_fcurve_is_empty(fcu)) { AnimData *adt = ale->adt; blender::animrig::animdata_fcurve_delete(adt, fcu); ale->key_data = nullptr; } } } } /** * Find the first segment of consecutive selected curve points, starting from \a start_index. * Keys that have BEZT_FLAG_IGNORE_TAG set are treated as unselected. * \param r_segment_start_idx: returns the start index of the segment. * \param r_segment_len: returns the number of curve points in the segment. * \return whether such a segment was found or not. */ static bool find_fcurve_segment(FCurve *fcu, const int start_index, int *r_segment_start_idx, int *r_segment_len) { *r_segment_start_idx = 0; *r_segment_len = 0; bool in_segment = false; for (int i = start_index; i < fcu->totvert; i++) { const bool point_is_selected = fcu->bezt[i].f2 & SELECT; const bool point_is_ignored = fcu->bezt[i].f2 & BEZT_FLAG_IGNORE_TAG; if (point_is_selected && !point_is_ignored) { if (!in_segment) { *r_segment_start_idx = i; in_segment = true; } (*r_segment_len)++; } else if (in_segment) { /* If the curve point is not selected then we have reached the end of the selected curve * segment. */ return true; /* Segment found. */ } } /* If the last curve point was in the segment, `r_segment_len` and `r_segment_start_idx` * are already updated and true is returned. */ return in_segment; } ListBase find_fcurve_segments(FCurve *fcu) { ListBase segments = {nullptr, nullptr}; /* Ignore baked curves. */ if (!fcu->bezt) { return segments; } int segment_start_idx = 0; int segment_len = 0; int current_index = 0; while (find_fcurve_segment(fcu, current_index, &segment_start_idx, &segment_len)) { FCurveSegment *segment; segment = static_cast(MEM_callocN(sizeof(*segment), "FCurveSegment")); segment->start_index = segment_start_idx; segment->length = segment_len; BLI_addtail(&segments, segment); current_index = segment_start_idx + segment_len; } return segments; } static const BezTriple *fcurve_segment_start_get(FCurve *fcu, int index) { const BezTriple *start_bezt = index - 1 >= 0 ? &fcu->bezt[index - 1] : &fcu->bezt[index]; return start_bezt; } static const BezTriple *fcurve_segment_end_get(FCurve *fcu, int index) { const BezTriple *end_bezt = index < fcu->totvert ? &fcu->bezt[index] : &fcu->bezt[index - 1]; return end_bezt; } /* ---------------- */ void blend_to_neighbor_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *target_bezt; /* Find which key to blend towards. */ if (factor < 0) { target_bezt = fcurve_segment_start_get(fcu, segment->start_index); } else { target_bezt = fcurve_segment_end_get(fcu, segment->start_index + segment->length); } const float lerp_factor = fabs(factor); /* Blend each key individually. */ for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float key_y_value = interpf(target_bezt->vec[1][1], fcu->bezt[i].vec[1][1], lerp_factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ float get_default_rna_value(const FCurve *fcu, PropertyRNA *prop, PointerRNA *ptr) { const int len = RNA_property_array_length(ptr, prop); float default_value = 0; /* Find the default value of that property. */ switch (RNA_property_type(prop)) { case PROP_BOOLEAN: if (len) { default_value = float(RNA_property_boolean_get_default_index(ptr, prop, fcu->array_index)); } else { default_value = float(RNA_property_boolean_get_default(ptr, prop)); } break; case PROP_INT: if (len) { default_value = RNA_property_int_get_default_index(ptr, prop, fcu->array_index); } else { default_value = RNA_property_int_get_default(ptr, prop); } break; case PROP_FLOAT: if (len) { default_value = RNA_property_float_get_default_index(ptr, prop, fcu->array_index); } else { default_value = RNA_property_float_get_default(ptr, prop); } break; default: break; } return default_value; } void blend_to_default_fcurve(PointerRNA *id_ptr, FCurve *fcu, const float factor) { PointerRNA ptr; PropertyRNA *prop; /* Check if path is valid. */ if (!RNA_path_resolve_property(id_ptr, fcu->rna_path, &ptr, &prop)) { return; } const float default_value = get_default_rna_value(fcu, prop, &ptr); /* Blend selected keys to default. */ for (int i = 0; i < fcu->totvert; i++) { if (!(fcu->bezt[i].f2 & SELECT)) { continue; } const float key_y_value = interpf(default_value, fcu->bezt[i].vec[1][1], factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ void scale_average_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { float y = 0; /* Find first the average of the y values to then use it in the final calculation. */ for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { y += fcu->bezt[i].vec[1][1]; } const float y_average = y / segment->length; for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float key_y_value = interpf(y_average, fcu->bezt[i].vec[1][1], 1 - factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ struct ButterworthCoefficients { double *A, *d1, *d2; int filter_order; }; ButterworthCoefficients *ED_anim_allocate_butterworth_coefficients(const int filter_order) { ButterworthCoefficients *bw_coeff = static_cast( MEM_callocN(sizeof(ButterworthCoefficients), "Butterworth Coefficients")); bw_coeff->filter_order = filter_order; bw_coeff->d1 = static_cast( MEM_callocN(sizeof(double) * filter_order, "coeff filtered")); bw_coeff->d2 = static_cast( MEM_callocN(sizeof(double) * filter_order, "coeff samples")); bw_coeff->A = static_cast(MEM_callocN(sizeof(double) * filter_order, "Butterworth A")); return bw_coeff; } void ED_anim_free_butterworth_coefficients(ButterworthCoefficients *bw_coeff) { MEM_freeN(bw_coeff->d1); MEM_freeN(bw_coeff->d2); MEM_freeN(bw_coeff->A); MEM_freeN(bw_coeff); } void ED_anim_calculate_butterworth_coefficients(const float cutoff_frequency, const float sampling_frequency, ButterworthCoefficients *bw_coeff) { double s = double(sampling_frequency); const double a = tan(M_PI * cutoff_frequency / s); const double a2 = a * a; double r; for (int i = 0; i < bw_coeff->filter_order; ++i) { r = sin(M_PI * (2.0 * i + 1.0) / (4.0 * bw_coeff->filter_order)); s = a2 + 2.0 * a * r + 1.0; bw_coeff->A[i] = a2 / s; bw_coeff->d1[i] = 2.0 * (1 - a2) / s; bw_coeff->d2[i] = -(a2 - 2.0 * a * r + 1.0) / s; } } static double butterworth_filter_value( double x, double *w0, double *w1, double *w2, ButterworthCoefficients *bw_coeff) { for (int i = 0; i < bw_coeff->filter_order; i++) { w0[i] = bw_coeff->d1[i] * w1[i] + bw_coeff->d2[i] * w2[i] + x; x = bw_coeff->A[i] * (w0[i] + 2.0 * w1[i] + w2[i]); w2[i] = w1[i]; w1[i] = w0[i]; } return x; } static float butterworth_calculate_blend_value(float *samples, const float *filtered_values, const int start_index, const int end_index, const int sample_index, const int blend_in_out) { if (start_index == end_index || blend_in_out == 0) { return samples[start_index]; } const float blend_in_y_samples = samples[start_index]; const float blend_out_y_samples = samples[end_index]; const float blend_in_y_filtered = filtered_values[start_index + blend_in_out]; const float blend_out_y_filtered = filtered_values[end_index - blend_in_out]; const float slope_in_samples = samples[start_index] - samples[start_index - 1]; const float slope_out_samples = samples[end_index] - samples[end_index + 1]; const float slope_in_filtered = filtered_values[start_index + blend_in_out - 1] - filtered_values[start_index + blend_in_out]; const float slope_out_filtered = filtered_values[end_index - blend_in_out] - filtered_values[end_index - blend_in_out - 1]; if (sample_index - start_index <= blend_in_out) { const int blend_index = sample_index - start_index; const float blend_in_out_factor = clamp_f(float(blend_index) / blend_in_out, 0.0f, 1.0f); const float blend_value = interpf(blend_in_y_filtered + slope_in_filtered * (blend_in_out - blend_index), blend_in_y_samples + slope_in_samples * blend_index, blend_in_out_factor); return blend_value; } if (end_index - sample_index <= blend_in_out) { const int blend_index = end_index - sample_index; const float blend_in_out_factor = clamp_f(float(blend_index) / blend_in_out, 0.0f, 1.0f); const float blend_value = interpf(blend_out_y_filtered + slope_out_filtered * (blend_in_out - blend_index), blend_out_y_samples + slope_out_samples * blend_index, blend_in_out_factor); return blend_value; } return 0; } void butterworth_smooth_fcurve_segment(FCurve *fcu, FCurveSegment *segment, float *samples, const int sample_count, const float factor, const int blend_in_out, const int sample_rate, ButterworthCoefficients *bw_coeff) { const int filter_order = bw_coeff->filter_order; float *filtered_values = static_cast( MEM_callocN(sizeof(float) * sample_count, "Butterworth Filtered FCurve Values")); double *w0 = static_cast(MEM_callocN(sizeof(double) * filter_order, "w0")); double *w1 = static_cast(MEM_callocN(sizeof(double) * filter_order, "w1")); double *w2 = static_cast(MEM_callocN(sizeof(double) * filter_order, "w2")); /* The values need to be offset so the first sample starts at 0. This avoids oscillations at the * start and end of the curve. */ const float fwd_offset = samples[0]; for (int i = 0; i < sample_count; i++) { const double x = double(samples[i] - fwd_offset); const double filtered_value = butterworth_filter_value(x, w0, w1, w2, bw_coeff); filtered_values[i] = float(filtered_value) + fwd_offset; } for (int i = 0; i < filter_order; i++) { w0[i] = 0.0; w1[i] = 0.0; w2[i] = 0.0; } const float bwd_offset = filtered_values[sample_count - 1]; /* Run the filter backwards as well to remove phase offset. */ for (int i = sample_count - 1; i >= 0; i--) { const double x = double(filtered_values[i] - bwd_offset); const double filtered_value = butterworth_filter_value(x, w0, w1, w2, bw_coeff); filtered_values[i] = float(filtered_value) + bwd_offset; } const int segment_end_index = segment->start_index + segment->length; BezTriple left_bezt = fcu->bezt[segment->start_index]; BezTriple right_bezt = fcu->bezt[segment_end_index - 1]; const int samples_start_index = filter_order * sample_rate; const int samples_end_index = int(right_bezt.vec[1][0] - left_bezt.vec[1][0] + filter_order) * sample_rate; const int blend_in_out_clamped = min_ii(blend_in_out, (samples_end_index - samples_start_index) / 2); for (int i = segment->start_index; i < segment_end_index; i++) { float blend_in_out_factor; if (blend_in_out_clamped == 0) { blend_in_out_factor = 1; } else if (i < segment->start_index + segment->length / 2) { blend_in_out_factor = min_ff(float(i - segment->start_index) / blend_in_out_clamped, 1.0f); } else { blend_in_out_factor = min_ff(float(segment_end_index - i - 1) / blend_in_out_clamped, 1.0f); } const float x_delta = fcu->bezt[i].vec[1][0] - left_bezt.vec[1][0] + filter_order; /* Using round() instead of casting to int. Casting would introduce a stepping issue when the * x-value is just below a full frame. */ const int filter_index = round(x_delta * sample_rate); const float blend_value = butterworth_calculate_blend_value(samples, filtered_values, samples_start_index, samples_end_index, filter_index, blend_in_out_clamped); const float blended_value = interpf( filtered_values[filter_index], blend_value, blend_in_out_factor); const float key_y_value = interpf(blended_value, samples[filter_index], factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } MEM_freeN(filtered_values); MEM_freeN(w0); MEM_freeN(w1); MEM_freeN(w2); } /* ---------------- */ void ED_ANIM_get_1d_gauss_kernel(const float sigma, const int kernel_size, double *r_kernel) { BLI_assert(sigma > 0.0f); BLI_assert(kernel_size > 0); const double sigma_sq = 2.0 * sigma * sigma; double sum = 0.0; for (int i = 0; i < kernel_size; i++) { const double normalized_index = double(i) / (kernel_size - 1); r_kernel[i] = exp(-normalized_index * normalized_index / sigma_sq); if (i == 0) { sum += r_kernel[i]; } else { /* We only calculate half the kernel, * the normalization needs to take that into account. */ sum += r_kernel[i] * 2; } } /* Normalize kernel values. */ for (int i = 0; i < kernel_size; i++) { r_kernel[i] /= sum; } } void smooth_fcurve_segment(FCurve *fcu, FCurveSegment *segment, float *samples, const float factor, const int kernel_size, const double *kernel) { const int segment_end_index = segment->start_index + segment->length; const float segment_start_x = fcu->bezt[segment->start_index].vec[1][0]; for (int i = segment->start_index; i < segment_end_index; i++) { /* Using round() instead of (int). The latter would create stepping on x-values that are just * below a full frame. */ const int sample_index = round(fcu->bezt[i].vec[1][0] - segment_start_x) + kernel_size; /* Apply the kernel. */ double filter_result = samples[sample_index] * kernel[0]; for (int j = 1; j <= kernel_size; j++) { const double kernel_value = kernel[j]; filter_result += samples[sample_index + j] * kernel_value; filter_result += samples[sample_index - j] * kernel_value; } const float key_y_value = interpf(float(filter_result), samples[sample_index], factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ static float ease_sigmoid_function(const float x, const float width, const float shift) { const float x_shift = (x - shift) * width; const float y = x_shift / sqrt(1 + pow2f(x_shift)); /* Normalize result to 0-1. */ return (y + 1) * 0.5f; } void ease_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor, const float width) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); const float key_x_range = right_key->vec[1][0] - left_key->vec[1][0]; const float key_y_range = right_key->vec[1][1] - left_key->vec[1][1]; /* Happens if there is only 1 key on the FCurve. Needs to be skipped because it * would be a divide by 0. */ if (IS_EQF(key_x_range, 0.0f)) { return; } /* Using the factor on the X-shift we are basically moving the curve horizontally. */ const float shift = -factor; const float y_min = ease_sigmoid_function(-1, width, shift); const float y_max = ease_sigmoid_function(1, width, shift); for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { /* Mapping the x-location of the key within the segment to a -1/1 range. */ const float x = ((fcu->bezt[i].vec[1][0] - left_key->vec[1][0]) / key_x_range) * 2 - 1; const float y = ease_sigmoid_function(x, width, shift); /* Normalizing the y value to the min and max to ensure that the keys at the end are not * detached from the rest of the animation. */ const float blend = (y - y_min) * (1 / (y_max - y_min)); const float key_y_value = left_key->vec[1][1] + key_y_range * blend; BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ void blend_offset_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); float y_delta; if (factor > 0) { const BezTriple segment_last_key = fcu->bezt[segment->start_index + segment->length - 1]; y_delta = right_key->vec[1][1] - segment_last_key.vec[1][1]; } else { const BezTriple segment_first_key = fcu->bezt[segment->start_index]; y_delta = left_key->vec[1][1] - segment_first_key.vec[1][1]; } const float offset_value = y_delta * fabs(factor); for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float key_y_value = fcu->bezt[i].vec[1][1] + offset_value; BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ static float s_curve(float x, float slope, float width, float height, float xshift, float yshift) { /* Formula for 'S' curve we use for the "ease" sliders. * The shift values move the curve vertically or horizontally. * The range of the curve used is from 0 to 1 on "x" and "y" * so we can scale it (width and height) and move it (`xshift` and y `yshift`) * to crop the part of the curve we need. Slope determines how curvy the shape is. */ float y = height * pow((x - xshift), slope) / (pow((x - xshift), slope) + pow((width - (x - xshift)), slope)) + yshift; /* The curve doesn't do what we want beyond our margins so we clamp the values. */ if (x > xshift + width) { y = height + yshift; } else if (x < xshift) { y = yshift; } return y; } void blend_to_ease_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); const float key_x_range = right_key->vec[1][0] - left_key->vec[1][0]; const float key_y_range = right_key->vec[1][1] - left_key->vec[1][1]; /* Happens if there is only 1 key on the FCurve. Needs to be skipped because it * would be a divide by 0. */ if (IS_EQF(key_x_range, 0.0f)) { return; } const float slope = 3.0; /* By doubling the size of the "S" curve we just one side of it, a "C" shape. */ const float width = 2.0; const float height = 2.0; float xy_shift; /* Shifting the x and y values we can decide what side of the "S" shape to use. */ if (factor > 0) { xy_shift = -1.0; } else { xy_shift = 0.0; } for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float x = (fcu->bezt[i].vec[1][0] - left_key->vec[1][0]) / key_x_range; const float ease = s_curve(x, slope, width, height, xy_shift, xy_shift); const float base = left_key->vec[1][1] + key_y_range * ease; float y_delta; if (factor > 0) { y_delta = base - fcu->bezt[i].vec[1][1]; } else { y_delta = fcu->bezt[i].vec[1][1] - base; } const float key_y_value = fcu->bezt[i].vec[1][1] + y_delta * factor; BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ bool match_slope_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); BezTriple beyond_key; const BezTriple *reference_key; if (factor >= 0) { /* Stop the function if there is no key beyond the right neighboring one. */ if (segment->start_index + segment->length >= fcu->totvert - 1) { return false; } reference_key = right_key; beyond_key = fcu->bezt[segment->start_index + segment->length + 1]; } else { /* Stop the function if there is no key beyond the left neighboring one. */ if (segment->start_index <= 1) { return false; } reference_key = left_key; beyond_key = fcu->bezt[segment->start_index - 2]; } /* This delta values are used to get the relationship between the bookend keys and the * reference keys beyond those. */ const float y_delta = beyond_key.vec[1][1] - reference_key->vec[1][1]; const float x_delta = beyond_key.vec[1][0] - reference_key->vec[1][0]; /* Avoids dividing by 0. */ if (x_delta == 0) { return false; } for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { /* These new deltas are used to determine the relationship between the current key and the * bookend ones. */ const float new_x_delta = fcu->bezt[i].vec[1][0] - reference_key->vec[1][0]; const float new_y_delta = new_x_delta * y_delta / x_delta; const float delta = reference_key->vec[1][1] + new_y_delta - fcu->bezt[i].vec[1][1]; const float key_y_value = fcu->bezt[i].vec[1][1] + delta * fabs(factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } return true; } /* ---------------- */ void shear_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor, tShearDirection direction) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); const float key_x_range = right_key->vec[1][0] - left_key->vec[1][0]; const float key_y_range = right_key->vec[1][1] - left_key->vec[1][1]; /* Happens if there is only 1 key on the FCurve. Needs to be skipped because it * would be a divide by 0. */ if (IS_EQF(key_x_range, 0.0f)) { return; } for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { /* For easy calculation of the curve, the values are normalized. */ float normalized_x; if (direction == SHEAR_FROM_LEFT) { normalized_x = (fcu->bezt[i].vec[1][0] - left_key->vec[1][0]) / key_x_range; } else { normalized_x = (right_key->vec[1][0] - fcu->bezt[i].vec[1][0]) / key_x_range; } const float y_delta = key_y_range * normalized_x; const float key_y_value = fcu->bezt[i].vec[1][1] + y_delta * factor; BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ void push_pull_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *left_key = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); const float key_x_range = right_key->vec[1][0] - left_key->vec[1][0]; const float key_y_range = right_key->vec[1][1] - left_key->vec[1][1]; /* Happens if there is only 1 key on the FCurve. Needs to be skipped because it * would be a divide by 0. */ if (IS_EQF(key_x_range, 0.0f)) { return; } for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { /* For easy calculation of the curve, the values are normalized. */ const float normalized_x = (fcu->bezt[i].vec[1][0] - left_key->vec[1][0]) / key_x_range; const float linear = left_key->vec[1][1] + key_y_range * normalized_x; const float delta = fcu->bezt[i].vec[1][1] - linear; const float key_y_value = linear + delta * factor; BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ void time_offset_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float frame_offset) { /* Two bookend keys of the fcurve are needed to be able to cycle the values. */ const BezTriple *last_key = &fcu->bezt[fcu->totvert - 1]; const BezTriple *first_key = &fcu->bezt[0]; const float fcu_x_range = last_key->vec[1][0] - first_key->vec[1][0]; const float fcu_y_range = last_key->vec[1][1] - first_key->vec[1][1]; const float first_key_x = first_key->vec[1][0]; /* If we operate directly on the fcurve there will be a feedback loop * so we need to capture the "y" values on an array to then apply them on a second loop. */ float *y_values = static_cast( MEM_callocN(sizeof(float) * segment->length, "Time Offset Samples")); for (int i = 0; i < segment->length; i++) { /* This simulates the fcu curve moving in time. */ const float time = fcu->bezt[segment->start_index + i].vec[1][0] + frame_offset; /* Need to normalize time to first_key to specify that as the wrapping point. */ const float wrapped_time = floored_fmod(time - first_key_x, fcu_x_range) + first_key_x; const float delta_y = fcu_y_range * floorf((time - first_key_x) / fcu_x_range); const float key_y_value = evaluate_fcurve(fcu, wrapped_time) + delta_y; y_values[i] = key_y_value; } for (int i = 0; i < segment->length; i++) { BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[segment->start_index + i], y_values[i]); } MEM_freeN(y_values); } /* ---------------- */ void scale_from_fcurve_segment_neighbor(FCurve *fcu, FCurveSegment *segment, const float factor, const FCurveSegmentAnchor anchor) { const BezTriple *reference_key; switch (anchor) { case FCurveSegmentAnchor::LEFT: reference_key = fcurve_segment_start_get(fcu, segment->start_index); break; case FCurveSegmentAnchor::RIGHT: reference_key = fcurve_segment_end_get(fcu, segment->start_index + segment->length); break; } for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float key_y_value = interpf(fcu->bezt[i].vec[1][1], reference_key->vec[1][1], factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /* ---------------- */ void breakdown_fcurve_segment(FCurve *fcu, FCurveSegment *segment, const float factor) { const BezTriple *left_bezt = fcurve_segment_start_get(fcu, segment->start_index); const BezTriple *right_bezt = fcurve_segment_end_get(fcu, segment->start_index + segment->length); const float lerp_factor = (factor + 1) / 2; for (int i = segment->start_index; i < segment->start_index + segment->length; i++) { const float key_y_value = interpf(right_bezt->vec[1][1], left_bezt->vec[1][1], lerp_factor); BKE_fcurve_keyframe_move_value_with_handles(&fcu->bezt[i], key_y_value); } } /** \} */ /* -------------------------------------------------------------------- */ /** \name FCurve Decimate * \{ */ /* Check if the keyframe interpolation type is supported */ static bool prepare_for_decimate(FCurve *fcu, int i) { switch (fcu->bezt[i].ipo) { case BEZT_IPO_BEZ: /* We do not need to do anything here as the keyframe already has the required setting. */ return true; case BEZT_IPO_LIN: /* Convert to a linear bezt curve to be able to use the decimation algorithm. */ fcu->bezt[i].ipo = BEZT_IPO_BEZ; fcu->bezt[i].h1 = HD_FREE; fcu->bezt[i].h2 = HD_FREE; if (i != 0) { float h1[3]; sub_v3_v3v3(h1, fcu->bezt[i - 1].vec[1], fcu->bezt[i].vec[1]); mul_v3_fl(h1, 1.0f / 3.0f); add_v3_v3(h1, fcu->bezt[i].vec[1]); copy_v3_v3(fcu->bezt[i].vec[0], h1); } if (i + 1 != fcu->totvert) { float h2[3]; sub_v3_v3v3(h2, fcu->bezt[i + 1].vec[1], fcu->bezt[i].vec[1]); mul_v3_fl(h2, 1.0f / 3.0f); add_v3_v3(h2, fcu->bezt[i].vec[1]); copy_v3_v3(fcu->bezt[i].vec[2], h2); } return true; default: /* These are unsupported. */ return false; } } /* Decimate the given curve segment. */ static void decimate_fcurve_segment(FCurve *fcu, int bezt_segment_start_idx, int bezt_segment_len, float remove_ratio, float error_sq_max) { int selected_len = bezt_segment_len; /* Make sure that we can remove the start/end point of the segment if they * are not the start/end point of the curve. BKE_curve_decimate_bezt_array * has a check that prevents removal of the first and last index in the * passed array. */ if (bezt_segment_len + bezt_segment_start_idx != fcu->totvert && prepare_for_decimate(fcu, bezt_segment_len + bezt_segment_start_idx)) { bezt_segment_len++; } if (bezt_segment_start_idx != 0 && prepare_for_decimate(fcu, bezt_segment_start_idx - 1)) { bezt_segment_start_idx--; bezt_segment_len++; } const int target_fcurve_verts = ceil(bezt_segment_len - selected_len * remove_ratio); BKE_curve_decimate_bezt_array(&fcu->bezt[bezt_segment_start_idx], bezt_segment_len, 12, /* The actual resolution displayed in the viewport is dynamic * so we just pick a value that preserves the curve shape. */ false, SELECT, BEZT_FLAG_TEMP_TAG, error_sq_max, target_fcurve_verts); } bool decimate_fcurve(bAnimListElem *ale, float remove_ratio, float error_sq_max) { FCurve *fcu = static_cast(ale->key_data); /* Check if the curve actually has any points. */ if (fcu == nullptr || fcu->bezt == nullptr || fcu->totvert == 0) { return true; } BezTriple *old_bezts = fcu->bezt; bool can_decimate_all_selected = true; for (int i = 0; i < fcu->totvert; i++) { /* Ignore keyframes that are not supported. */ if (!prepare_for_decimate(fcu, i)) { can_decimate_all_selected = false; fcu->bezt[i].f2 |= BEZT_FLAG_IGNORE_TAG; } /* Make sure that the temp flag is unset as we use it to determine what to remove. */ fcu->bezt[i].f2 &= ~BEZT_FLAG_TEMP_TAG; } ListBase segments = find_fcurve_segments(fcu); LISTBASE_FOREACH (FCurveSegment *, segment, &segments) { decimate_fcurve_segment( fcu, segment->start_index, segment->length, remove_ratio, error_sq_max); } BLI_freelistN(&segments); uint old_totvert = fcu->totvert; fcu->bezt = nullptr; fcu->totvert = 0; for (int i = 0; i < old_totvert; i++) { BezTriple *bezt = (old_bezts + i); bezt->f2 &= ~BEZT_FLAG_IGNORE_TAG; if ((bezt->f2 & BEZT_FLAG_TEMP_TAG) == 0) { blender::animrig::insert_bezt_fcurve(fcu, bezt, eInsertKeyFlags(0)); } } /* now free the memory used by the old BezTriples */ if (old_bezts) { MEM_freeN(old_bezts); } return can_decimate_all_selected; } /** \} */ /* -------------------------------------------------------------------- */ /** \name FCurve Smooth * \{ */ /* temp struct used for smooth_fcurve */ struct tSmooth_Bezt { float *h1, *h2, *h3; /* bezt->vec[0,1,2][1] */ float y1, y2, y3; /* averaged before/new/after y-values */ }; void smooth_fcurve(FCurve *fcu) { int totSel = 0; if (fcu->bezt == nullptr) { return; } /* first loop through - count how many verts are selected */ BezTriple *bezt = fcu->bezt; for (int i = 0; i < fcu->totvert; i++, bezt++) { if (BEZT_ISSEL_ANY(bezt)) { totSel++; } } /* if any points were selected, allocate tSmooth_Bezt points to work on */ if (totSel >= 3) { tSmooth_Bezt *tarray, *tsb; /* allocate memory in one go */ tsb = tarray = static_cast( MEM_callocN(totSel * sizeof(tSmooth_Bezt), "tSmooth_Bezt Array")); /* populate tarray with data of selected points */ bezt = fcu->bezt; for (int i = 0, x = 0; (i < fcu->totvert) && (x < totSel); i++, bezt++) { if (BEZT_ISSEL_ANY(bezt)) { /* tsb simply needs pointer to vec, and index */ tsb->h1 = &bezt->vec[0][1]; tsb->h2 = &bezt->vec[1][1]; tsb->h3 = &bezt->vec[2][1]; /* advance to the next tsb to populate */ if (x < totSel - 1) { tsb++; } else { break; } } } /* calculate the new smoothed F-Curve's with weighted averages: * - this is done with two passes to avoid progressive corruption errors * - uses 5 points for each operation (which stores in the relevant handles) * - previous: w/a ratio = 3:5:2:1:1 * - next: w/a ratio = 1:1:2:5:3 */ /* round 1: calculate smoothing deltas and new values */ tsb = tarray; for (int i = 0; i < totSel; i++, tsb++) { /* Don't touch end points (otherwise, curves slowly explode, * as we don't have enough data there). */ if (ELEM(i, 0, (totSel - 1)) == 0) { const tSmooth_Bezt *tP1 = tsb - 1; const tSmooth_Bezt *tP2 = (i - 2 > 0) ? (tsb - 2) : (nullptr); const tSmooth_Bezt *tN1 = tsb + 1; const tSmooth_Bezt *tN2 = (i + 2 < totSel) ? (tsb + 2) : (nullptr); const float p1 = *tP1->h2; const float p2 = (tP2) ? (*tP2->h2) : (*tP1->h2); const float c1 = *tsb->h2; const float n1 = *tN1->h2; const float n2 = (tN2) ? (*tN2->h2) : (*tN1->h2); /* calculate previous and next, then new position by averaging these */ tsb->y1 = (3 * p2 + 5 * p1 + 2 * c1 + n1 + n2) / 12; tsb->y3 = (p2 + p1 + 2 * c1 + 5 * n1 + 3 * n2) / 12; tsb->y2 = (tsb->y1 + tsb->y3) / 2; } } /* round 2: apply new values */ tsb = tarray; for (int i = 0; i < totSel; i++, tsb++) { /* don't touch end points, as their values weren't touched above */ if (ELEM(i, 0, (totSel - 1)) == 0) { /* y2 takes the average of the 2 points */ *tsb->h2 = tsb->y2; /* handles are weighted between their original values and the averaged values */ *tsb->h1 = ((*tsb->h1) * 0.7f) + (tsb->y1 * 0.3f); *tsb->h3 = ((*tsb->h3) * 0.7f) + (tsb->y3 * 0.3f); } } /* free memory required for tarray */ MEM_freeN(tarray); } /* recalculate handles */ BKE_fcurve_handles_recalc(fcu); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Copy/Paste Tools * * - The copy/paste buffer currently stores a set of temporary F-Curves containing only the * keyframes that were selected in each of the original F-Curves. * - All pasted frames are offset by the same amount. * This is calculated as the difference in the times of the current frame and the * `first keyframe` (i.e. the earliest one in all channels). * - The earliest frame is calculated per copy operation. * \{ */ namespace blender::ed::animation { KeyframeCopyBuffer *keyframe_copy_buffer = nullptr; bool KeyframeCopyBuffer::is_empty() const { /* No need to check the channelbags for having F-Curves, as they are only * added when an F-Curve needs to be stored. */ return this->keyframe_data.channelbags().is_empty(); } bool KeyframeCopyBuffer::is_single_fcurve() const { if (this->keyframe_data.channelbags().size() != 1) { return false; } const animrig::Channelbag *channelbag = this->keyframe_data.channelbag(0); return channelbag->fcurves().size() == 1; } bool KeyframeCopyBuffer::is_bone(const FCurve &fcurve) const { return this->bone_fcurves.contains(&fcurve); } int KeyframeCopyBuffer::num_slots() const { /* The number of slots can be taken from any of these properties, so just assert that they are * consistent with each other. */ BLI_assert(this->keyframe_data.channelbags().size() == this->slot_identifiers.size()); /* Return the number of channelbags, as this is actually storing data; the `slot_identifiers` * field is more cache-like. Even though they should produce the same value, I (Sybren) feel more * comfortable using the channelbag count, as channelbags are defined as per-slot F-Curve * storage. */ return this->keyframe_data.channelbags().size(); } animrig::Channelbag *KeyframeCopyBuffer::channelbag_for_slot(const StringRef slot_identifier) { /* TODO: use a nicer data structure so this loop isn't necessary any more. Or use a vector, in * which case we always need to loop, but it'll be small and maybe the lower overhead of Vector * (vs Map) will be worth it anyway. */ for (const auto [handle, identifier] : this->slot_identifiers.items()) { if (identifier == slot_identifier) { return this->keyframe_data.channelbag_for_slot(handle); } } return nullptr; } void KeyframeCopyBuffer::debug_print() const { using namespace blender::animrig; std::cout << "KeyframeCopyBuffer contents:" << std::endl; std::cout << " frame range: " << this->first_frame << "-" << this->last_frame << std::endl; std::cout << " scene frame: " << this->current_frame << std::endl; if (is_empty()) { std::cout << " buffer is empty" << std::endl; } if (is_single_fcurve()) { std::cout << " buffer has single F-Curve" << std::endl; } const StripKeyframeData &keyframe_data = this->keyframe_data; std::cout << " channelbags: " << keyframe_data.channelbags().size() << std::endl; for (const Channelbag *channelbag : keyframe_data.channelbags()) { const std::string &slot_identifier = this->slot_identifiers.lookup(channelbag->slot_handle); if (slot_identifier == KeyframeCopyBuffer::SLOTLESS_SLOT_IDENTIFIER) { std::cout << " - Channelbag for slotless F-Curves:" << std::endl; } else { std::cout << " - Channelbag for slot \"" << slot_identifier << "\":" << std::endl; } for (const FCurve *fcurve : channelbag->fcurves()) { std::cout << " " << fcurve->rna_path << "[" << fcurve->array_index << "]"; if (this->is_bone(*fcurve)) { std::cout << " (bone)"; } std::cout << std::endl; } } } } // namespace blender::ed::animation void ANIM_fcurves_copybuf_reset() { using namespace blender::ed::animation; ANIM_fcurves_copybuf_free(); keyframe_copy_buffer = MEM_new(__func__); } void ANIM_fcurves_copybuf_free() { using namespace blender::ed::animation; if (keyframe_copy_buffer) { MEM_delete(keyframe_copy_buffer); } } /* ------------------- */ static bool is_animating_bone(const bAnimListElem *ale) { BLI_assert(ale->datatype == ALE_FCURVE); if (!ale->id || GS(ale->id->name) != ID_OB) { return false; } Object *ob = reinterpret_cast(ale->id); if (ob->type != OB_ARMATURE) { return false; } FCurve *fcurve = static_cast(ale->key_data); if (!fcurve->rna_path) { return false; } char bone_name[sizeof(bPoseChannel::name)]; if (!BLI_str_quoted_substr(fcurve->rna_path, "pose.bones[", bone_name, sizeof(bone_name))) { return false; } bPoseChannel *pchan = BKE_pose_channel_find_name(ob->pose, bone_name); return pchan != nullptr; }; namespace { using namespace blender; using namespace blender::animrig; using namespace blender::ed::animation; /** * Utility class to help map slots from the Actions data was copied from, to the slots used by the * copy-paste buffer. * * To the caller, this mapping is implicit, and is just reflected in the returned `Channelbag` for * some `bAnimListElem`. There is a 1:1 mapping in the copy-paste buffer between slots and their * channelbags, * * Technically this code could be part of KeyframeCopyBuffer. The nice thing about the current * structure is that the properties of this class are only accessible when copying, and once the * work is done, this gets destructed. The KeyframeCopyBuffer class only tracks data that is * relevant for pasting. */ class SlotMapper { public: KeyframeCopyBuffer &buffer; /** Mapping from action + slot handle to the buffer's internal slot handle. */ Map, slot_handle_t> orig_to_buffer_slots; /** * Slot handle used for slotless keyframes. * * NOTE: this is only used in this class, and NOT used by the KeyframeCopyBuffer data structure. * There, the Channelbag for slotless data can be found via * `keyframe_copy_buffer->channelbag_for_slot(SLOTLESS_SLOT_IDENTIFIER)`. * * These are for keyframes copied from F-Curves not owned by an Action, such as drivers and NLA * control curves. */ static constexpr animrig::slot_handle_t SLOTLESS_SLOT_HANDLE = 0; /** * Ensure a Channelbag exists in the keyframe copy buffer for the F-Curve this 'ale' points to. */ Channelbag &channelbag_for_ale(const bAnimListElem *ale) { /* Slots really only exist with F-Curves from Actions. */ const Action *map_key_action; slot_handle_t map_key_handle; if (GS(ale->fcurve_owner_id->name) == ID_AC) { map_key_action = &reinterpret_cast(ale->fcurve_owner_id)->wrap(); map_key_handle = ale->slot_handle; } else { map_key_action = nullptr; map_key_handle = SLOTLESS_SLOT_HANDLE; } const auto map_key = std::make_pair(map_key_action, map_key_handle); if (const std::optional opt_internal_slot_handle = this->orig_to_buffer_slots.lookup_try(map_key)) { /* There already is a slot for this, and that means there is a channelbag too. */ const slot_handle_t internal_slot_handle = *opt_internal_slot_handle; BLI_assert(this->buffer.slot_identifiers.contains(internal_slot_handle)); Channelbag *channelbag = this->buffer.keyframe_data.channelbag_for_slot( internal_slot_handle); BLI_assert_msg(channelbag, "If the slot exists, so should the channelbag"); return *channelbag; } /* Create a new Channelbag for this F-Curve. */ const slot_handle_t internal_slot_handle = ++this->buffer.last_used_slot_handle; Channelbag &channelbag = this->buffer.keyframe_data.channelbag_for_slot_add( internal_slot_handle); this->orig_to_buffer_slots.add_new(map_key, internal_slot_handle); /* Determine the slot identifier. */ StringRef slot_identifier; if (map_key_action) { const Slot *ale_slot = map_key_action->slot_for_handle(ale->slot_handle); BLI_assert_msg(ale_slot, "Slot for copied keyframes is expected to exist."); slot_identifier = ale_slot->identifier; } else { slot_identifier = KeyframeCopyBuffer::SLOTLESS_SLOT_IDENTIFIER; } this->buffer.slot_identifiers.add(internal_slot_handle, slot_identifier); /* ale->id might be nullptr on unassigned slots. */ this->buffer.slot_animated_ids.add_new(internal_slot_handle, ale->id); return channelbag; } }; } // namespace bool copy_animedit_keys(bAnimContext *ac, ListBase *anim_data) { using namespace blender::ed::animation; using namespace blender::animrig; ANIM_fcurves_copybuf_reset(); SlotMapper slot_mapper{*keyframe_copy_buffer}; LISTBASE_FOREACH (const bAnimListElem *, ale, anim_data) { BLI_assert(ale->datatype == ALE_FCURVE); const FCurve *fcu = static_cast(ale->key_data); /* Firstly, check if F-Curve has any selected keyframes. Skip if no selected * keyframes found (so no need to create unnecessary copy-buffer data). This * check should also eliminate any problems associated with using * sample-data. */ if (ANIM_fcurve_keyframes_loop( nullptr, /* The const-cast is because I (Sybren) want to have `fcu` as `const` in as much of * this LISTBASE_FOREACH as possible. The code is alternating between the to-be-copied * F-Curve and the copy, and I want the compiler to help distinguish those. */ const_cast(fcu), nullptr, ANIM_editkeyframes_ok(BEZT_OK_SELECTED), nullptr) == 0) { continue; } Channelbag &channelbag = slot_mapper.channelbag_for_ale(ale); /* Create an F-Curve on this ChannelBag. */ FCurve &fcurve_copy = *BKE_fcurve_create(); fcurve_copy.rna_path = BLI_strdup(fcu->rna_path); fcurve_copy.array_index = fcu->array_index; channelbag.fcurve_append(fcurve_copy); if (fcu->grp) { bActionGroup &group = channelbag.channel_group_ensure(fcu->grp->name); channelbag.fcurve_assign_to_channel_group(fcurve_copy, group); } /* Detect if this is a bone. We do that here rather than during pasting * because ID pointers will get invalidated on undo / loading another file. */ if (is_animating_bone(ale)) { keyframe_copy_buffer->bone_fcurves.add(&fcurve_copy); } /* Add selected keyframes to the buffer F-Curve. */ int bezt_index = 0; BezTriple *bezt = fcu->bezt; for (; bezt_index < fcu->totvert; bezt_index++, bezt++) { if (!BEZT_ISSEL_ANY(bezt)) { continue; } /* Use INSERTKEY_FAST as that avoids recalculating handles. They should * remain as-is in the buffer. */ animrig::insert_bezt_fcurve(&fcurve_copy, bezt, INSERTKEY_OVERWRITE_FULL | INSERTKEY_FAST); /* Keep track of the extremities. */ const float bezt_frame = bezt->vec[1][0]; keyframe_copy_buffer->first_frame = std::min(keyframe_copy_buffer->first_frame, bezt_frame); keyframe_copy_buffer->last_frame = std::max(keyframe_copy_buffer->last_frame, bezt_frame); } } keyframe_copy_buffer->current_frame = ac->scene->r.cfra; #ifndef NDEBUG /* TODO: remove this call completely when slot-aware copy-pasting has been implemented. */ keyframe_copy_buffer->debug_print(); #endif return !keyframe_copy_buffer->is_empty(); } namespace blender::ed::animation { std::optional flip_names(const blender::StringRefNull rna_path) { int ofs_start, ofs_end; if (!BLI_str_quoted_substr_range(rna_path.c_str(), "pose.bones[", &ofs_start, &ofs_end)) { return {}; } /* NOTE: there is no need to un-escape the string to flip it. * However the buffer does need to be twice the size. */ char bname_new[MAX_VGROUP_NAME * 2]; /* Take a copy so it's 0-terminated. */ const std::string bone_name = rna_path.substr(ofs_start, ofs_end - ofs_start); BLI_string_flip_side_name(bname_new, bone_name.c_str(), false, sizeof(bname_new)); return rna_path.substr(0, ofs_start) + bname_new + rna_path.substr(ofs_end); } using pastebuf_match_func = bool (*)(Main *bmain, const FCurve &fcurve_to_match, const FCurve &fcurve_in_copy_buffer, blender::animrig::slot_handle_t slot_handle_in_copy_buffer, bool from_single, bool to_single, bool flip); namespace { using namespace blender::animrig; enum class SlotMatchMethod { /** No matching, just ignore slots altogether. */ NONE = 0, /** Source and target F-Curve must be from a slot with the same identifier. */ IDENTIFIER = 1, /** Target F-Curve must be from a selected slot, name does not matter. */ SELECTION = 2, /** NAME + target F-Curve must be from a selected slot. */ SELECTION_AND_IDENTIFIER = 3, }; } // namespace /** * Determine whether slot names matter when matching copied data to selected-to-paste-into * channels. */ static SlotMatchMethod get_slot_match_method(const bool from_single, const bool to_single, const KeyframePasteContext &paste_context) { BLI_assert_msg(!(from_single && to_single), "The from-single-to-single case is expected to be implemented as a special case " "in `paste_animedit_keys()`"); UNUSED_VARS_NDEBUG(from_single); if (paste_context.num_fcurves_selected == 0) { /* No F-Curves selected to explicitly paste into. The names of the selected slots determine the * source Channelbag. */ if (paste_context.num_slots_selected == 0) { /* Since none of the slots were selected to paste into, just do a match by name and ignore * their selection state. */ return SlotMatchMethod::IDENTIFIER; } if (keyframe_copy_buffer->num_slots() == 1) { /* Copied from one slot, pasting into one or more selected slots. This should only look at * selection of the target slot, and ignore slot names. */ return SlotMatchMethod::SELECTION; } /* Copied from multiple slots, in which case slot names do matter, and the targets should be * limited by their slot selection (i.e. F-Curves from unselected slots should not be pasted * into). */ return SlotMatchMethod::SELECTION_AND_IDENTIFIER; } if (to_single) { /* Copying into a single F-Curve. Because the single-to-single case is handled somewhere else, * we know multiple F-Curves were copied. Slot names do not matter, matching is done purely on * RNA path + array index. * * TODO: slot names may matter here after all, when the copy buffer has multiple slots. In that * case the single F-Curve to paste into can still match multiple copied F-Curves. That's a * corner case to implement at some other time, though. */ return SlotMatchMethod::NONE; } /* Pasting into multiple F-Curves. Whether slot names matter depends on how many slots the * key were copied from: * - 0 slots: impossible, then there would not be any keys at all. * - 1 slot: slot names do not matter. This makes it possible to copy-paste between slots. * - 2+ slots: slot names matter, only paste within the same slot as the copied data. */ const int num_slots_copied = keyframe_copy_buffer->num_slots(); BLI_assert_msg(num_slots_copied > 0, "If any keyframes were copied, they MUST have come from some slot."); if (num_slots_copied > 1) { /* Copied from multiple slots, so do name matching. */ return SlotMatchMethod::IDENTIFIER; } return SlotMatchMethod::NONE; } /** * Return the first item in the copy buffer that matches the given bAnimListElem. * * \param ale_to_paste_into must be an ALE that represents an F-Curve. The entire ALE is passed * (instead of just the F-Curve) as it provides information about the Action & Slot it came from. */ static const FCurve *pastebuf_find_matching_copybuf_item(const pastebuf_match_func strategy, Main *bmain, const bAnimListElem &ale_to_paste_into, const bool from_single, const bool to_single, const KeyframePasteContext &paste_context) { using namespace blender::animrig; BLI_assert(ale_to_paste_into.datatype == ALE_FCURVE); const FCurve &fcurve_to_match = *static_cast(ale_to_paste_into.data); BLI_assert_msg(!(from_single && to_single), "The from-single-to-single case is expected to be implemented as a special case " "in `paste_animedit_keys()`"); /* Because `channelbags_to_paste_from` can reference `single_copy_buffer_channelbag`, the latter * has to live longer, hence it is declared first. */ const Channelbag *single_copy_buffer_channelbag; Span channelbags_to_paste_from; /* Get the slot of this ALE, as some of the cases below need to query it. It might be slotless, * for example for NLA control curves. */ const Slot *ale_slot = nullptr; const Action *ale_action = nullptr; if (GS(ale_to_paste_into.fcurve_owner_id->name) == ID_AC) { ale_action = &reinterpret_cast(ale_to_paste_into.fcurve_owner_id)->wrap(); ale_slot = ale_action->slot_for_handle(ale_to_paste_into.slot_handle); BLI_assert(ale_slot); } /* NASTYNESS: this code shouldn't have to care about which slots are currently visible in * the channel list. But since selection state is only relevant when they CAN actually be * selected, it does matter. This code assumes: * 1. because SELECTION or SELECTION_AND_IDENTIFIER was returned, slot selection is a * thing in this mode, * 2. because slot selection is a thing, and this F-Curve is potentially getting pasted * into, its slot is visible too, * 3. and because of that, the selection state of this slot is enough to check here. */ const SlotMatchMethod slot_match = get_slot_match_method(from_single, to_single, paste_context); switch (slot_match) { case SlotMatchMethod::SELECTION: if (!ale_slot->is_selected()) { return nullptr; } ATTR_FALLTHROUGH; case SlotMatchMethod::NONE: /* Just search through all channelbags in the copy buffer. */ channelbags_to_paste_from = keyframe_copy_buffer->keyframe_data.channelbags(); break; case SlotMatchMethod::SELECTION_AND_IDENTIFIER: if (!ale_slot->is_selected()) { return nullptr; } ATTR_FALLTHROUGH; case SlotMatchMethod::IDENTIFIER: { /* See if we copied from a slot whose identifier matches this ALE. */ const std::string target_slot_identifier = ale_slot ? ale_slot->identifier : KeyframeCopyBuffer::SLOTLESS_SLOT_IDENTIFIER; single_copy_buffer_channelbag = keyframe_copy_buffer->channelbag_for_slot( target_slot_identifier); if (!single_copy_buffer_channelbag) { /* No data copied from a slot with this name. */ return nullptr; } /* Only consider the F-Curves from this channelbag. Because of channelbags_to_paste_from * referencing single_copy_buffer_channelbag, the latter has to live longer, hence it was * declared first. */ channelbags_to_paste_from = Span(&single_copy_buffer_channelbag, 1); break; } } for (const Channelbag *channelbag : channelbags_to_paste_from) { for (const FCurve *fcurve : channelbag->fcurves()) { if (strategy(bmain, fcurve_to_match, *fcurve, channelbag->slot_handle, from_single, to_single, paste_context.flip)) { return fcurve; } } } return nullptr; } bool pastebuf_match_path_full(Main * /*bmain*/, const FCurve &fcurve_to_match, const FCurve &fcurve_in_copy_buffer, blender::animrig::slot_handle_t /*slot_handle_in_copy_buffer*/, const bool from_single, const bool to_single, const bool flip) { if (!fcurve_to_match.rna_path || !fcurve_in_copy_buffer.rna_path) { /* No paths to compare to, so only ok if pasting to a single F-Curve. */ return to_single; } /* The 'source' of the copy data is considered 'ok' if either we're copying a single F-Curve, * or the array index matches (so [location X,Y,Z] can be copied to a single 'scale' property, * and it'll pick up the right X/Y/Z component). */ const bool is_source_ok = from_single || fcurve_in_copy_buffer.array_index == fcurve_to_match.array_index; if (!is_source_ok) { return false; } if (!to_single && flip && keyframe_copy_buffer->is_bone(fcurve_in_copy_buffer)) { const std::optional with_flipped_name = blender::ed::animation::flip_names( fcurve_in_copy_buffer.rna_path); return with_flipped_name && with_flipped_name == fcurve_to_match.rna_path; } return to_single || STREQ(fcurve_in_copy_buffer.rna_path, fcurve_to_match.rna_path); } bool pastebuf_match_path_property(Main *bmain, const FCurve &fcurve_to_match, const FCurve &fcurve_in_copy_buffer, blender::animrig::slot_handle_t slot_handle_in_copy_buffer, const bool from_single, const bool /*to_single*/, const bool /*flip*/) { if (!fcurve_in_copy_buffer.rna_path || !fcurve_to_match.rna_path) { return false; } /* The 'source' of the copy data is considered 'ok' if either we're copying a single F-Curve, * or the array index matches (so [location X,Y,Z] can be copied to a single 'scale' property, * and it'll pick up the right X/Y/Z component). */ const bool is_source_ok = from_single || fcurve_in_copy_buffer.array_index == fcurve_to_match.array_index; if (!is_source_ok) { return false; } /* find the property of the fcurve and compare against the end of the tAnimCopybufItem * more involved since it needs to do path lookups. * This is not 100% reliable since the user could be editing the curves on a path that won't * resolve, or a bone could be renamed after copying for eg. but in normal copy & paste * this should work out ok. */ const std::optional optional_id = keyframe_copy_buffer->slot_animated_ids.lookup_try( slot_handle_in_copy_buffer); if (!optional_id) { printf( "paste_animedit_keys: no idea which ID was animated by \"%s\" in slot \"%s\", so cannot " "match by property name\n", fcurve_in_copy_buffer.rna_path, keyframe_copy_buffer->slot_identifiers.lookup(slot_handle_in_copy_buffer).c_str()); return false; } ID *animated_id = optional_id.value(); if (BLI_findindex(which_libbase(bmain, GS(animated_id->name)), animated_id) == -1) { /* The ID could have been removed after copying the keys. This function * needs it to resolve the property & get the name. */ printf("paste_animedit_keys: error ID has been removed!\n"); return false; } PointerRNA rptr; PropertyRNA *prop; PointerRNA id_ptr = RNA_id_pointer_create(animated_id); if (!RNA_path_resolve_property(&id_ptr, fcurve_in_copy_buffer.rna_path, &rptr, &prop)) { printf("paste_animedit_keys: failed to resolve path id:%s, '%s'!\n", animated_id->name, fcurve_in_copy_buffer.rna_path); return false; } const char *identifier = RNA_property_identifier(prop); /* NOTE: paths which end with "] will fail with this test - Animated ID Props. */ return blender::StringRef(fcurve_to_match.rna_path).endswith(identifier); } bool pastebuf_match_index_only(Main * /*bmain*/, const FCurve &fcurve_to_match, const FCurve &fcurve_in_copy_buffer, blender::animrig::slot_handle_t /* slot_handle_in_copy_buffer */, const bool from_single, const bool /*to_single*/, const bool /*flip*/) { return from_single || fcurve_in_copy_buffer.array_index == fcurve_to_match.array_index; } /* ................ */ static void do_curve_mirror_flippping(const FCurve &fcurve, BezTriple &bezt) { if (!keyframe_copy_buffer->is_bone(fcurve)) { return; } /* TODO: pull the investigation of the RNA path out of this function, and out of the loop over * all keys of the F-Curve. It only has to be done once per F-Curve, and not for every single * key. */ const size_t slength = strlen(fcurve.rna_path); bool flip = false; if (BLI_strn_endswith(fcurve.rna_path, "location", slength) && fcurve.array_index == 0) { flip = true; } else if (BLI_strn_endswith(fcurve.rna_path, "rotation_quaternion", slength) && ELEM(fcurve.array_index, 2, 3)) { flip = true; } else if (BLI_strn_endswith(fcurve.rna_path, "rotation_euler", slength) && ELEM(fcurve.array_index, 1, 2)) { flip = true; } else if (BLI_strn_endswith(fcurve.rna_path, "rotation_axis_angle", slength) && ELEM(fcurve.array_index, 2, 3)) { flip = true; } if (flip) { bezt.vec[0][1] = -bezt.vec[0][1]; bezt.vec[1][1] = -bezt.vec[1][1]; bezt.vec[2][1] = -bezt.vec[2][1]; } } /* helper for paste_animedit_keys() - performs the actual pasting */ static void paste_animedit_keys_fcurve(FCurve *fcu, const FCurve &fcurve_in_copy_buffer, float offset[2], const eKeyMergeMode merge_mode, bool flip) { BezTriple *bezt; int i; /* First de-select existing FCurve's keyframes */ for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) { BEZT_DESEL_ALL(bezt); } /* mix mode with existing data */ switch (merge_mode) { case KEYFRAME_PASTE_MERGE_MIX: /* do-nothing */ break; case KEYFRAME_PASTE_MERGE_OVER: /* remove all keys */ BKE_fcurve_delete_keys_all(fcu); break; case KEYFRAME_PASTE_MERGE_OVER_RANGE: case KEYFRAME_PASTE_MERGE_OVER_RANGE_ALL: { float f_min; float f_max; if (merge_mode == KEYFRAME_PASTE_MERGE_OVER_RANGE) { f_min = fcurve_in_copy_buffer.bezt[0].vec[1][0] + offset[0]; f_max = fcurve_in_copy_buffer.bezt[fcurve_in_copy_buffer.totvert - 1].vec[1][0] + offset[0]; } else { /* Entire Range */ f_min = keyframe_copy_buffer->first_frame + offset[0]; f_max = keyframe_copy_buffer->last_frame + offset[0]; } /* remove keys in range */ if (f_min < f_max) { /* select verts in range for removal */ for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) { if ((f_min < bezt[0].vec[1][0]) && (bezt[0].vec[1][0] < f_max)) { bezt->f2 |= SELECT; } } /* remove frames in the range */ BKE_fcurve_delete_keys_selected(fcu); } break; } } /* just start pasting, with the first keyframe on the current frame, and so on */ for (i = 0, bezt = fcurve_in_copy_buffer.bezt; i < fcurve_in_copy_buffer.totvert; i++, bezt++) { /* Create a copy to modify, before inserting it into the F-Curve. The * applied offset also determines the frame number of the pasted BezTriple. * If the insertion is done before the offset is applied, it will replace * the original key and _then_ move it to the new position. */ BezTriple bezt_copy = *bezt; if (flip) { do_curve_mirror_flippping(fcurve_in_copy_buffer, bezt_copy); } add_v2_v2(bezt_copy.vec[0], offset); add_v2_v2(bezt_copy.vec[1], offset); add_v2_v2(bezt_copy.vec[2], offset); /* Ensure that all pasted data is selected. */ BEZT_SEL_ALL(&bezt_copy); /* Only now that it has the right values, do the pasting into the F-Curve. */ blender::animrig::insert_bezt_fcurve(fcu, &bezt_copy, INSERTKEY_OVERWRITE_FULL); } /* recalculate F-Curve's handles? */ BKE_fcurve_handles_recalc(fcu); } } // namespace blender::ed::animation const EnumPropertyItem rna_enum_keyframe_paste_offset_items[] = { {KEYFRAME_PASTE_OFFSET_CFRA_START, "START", 0, "Frame Start", "Paste keys starting at current frame"}, {KEYFRAME_PASTE_OFFSET_CFRA_END, "END", 0, "Frame End", "Paste keys ending at current frame"}, {KEYFRAME_PASTE_OFFSET_CFRA_RELATIVE, "RELATIVE", 0, "Frame Relative", "Paste keys relative to the current frame when copying"}, {KEYFRAME_PASTE_OFFSET_NONE, "NONE", 0, "No Offset", "Paste keys from original time"}, {0, nullptr, 0, nullptr, nullptr}, }; const EnumPropertyItem rna_enum_keyframe_paste_offset_value_items[] = { {KEYFRAME_PASTE_VALUE_OFFSET_LEFT_KEY, "LEFT_KEY", 0, "Left Key", "Paste keys with the first key matching the key left of the cursor"}, {KEYFRAME_PASTE_VALUE_OFFSET_RIGHT_KEY, "RIGHT_KEY", 0, "Right Key", "Paste keys with the last key matching the key right of the cursor"}, {KEYFRAME_PASTE_VALUE_OFFSET_CFRA, "CURRENT_FRAME", 0, "Current Frame Value", "Paste keys relative to the value of the curve under the cursor"}, {KEYFRAME_PASTE_VALUE_OFFSET_CURSOR, "CURSOR_VALUE", 0, "Cursor Value", "Paste keys relative to the Y-Position of the cursor"}, {KEYFRAME_PASTE_VALUE_OFFSET_NONE, "NONE", 0, "No Offset", "Paste keys with the same value as they were copied"}, {0, nullptr, 0, nullptr, nullptr}, }; const EnumPropertyItem rna_enum_keyframe_paste_merge_items[] = { {KEYFRAME_PASTE_MERGE_MIX, "MIX", 0, "Mix", "Overlay existing with new keys"}, {KEYFRAME_PASTE_MERGE_OVER, "OVER_ALL", 0, "Overwrite All", "Replace all keys"}, {KEYFRAME_PASTE_MERGE_OVER_RANGE, "OVER_RANGE", 0, "Overwrite Range", "Overwrite keys in pasted range"}, {KEYFRAME_PASTE_MERGE_OVER_RANGE_ALL, "OVER_RANGE_ALL", 0, "Overwrite Entire Range", "Overwrite keys in pasted range, using the range of all copied keys"}, {0, nullptr, 0, nullptr, nullptr}, }; static float paste_get_y_offset(const bAnimContext *ac, const FCurve &fcurve_in_copy_buffer, const bAnimListElem *ale, const eKeyPasteValueOffset value_offset_mode) { BLI_assert(ale->datatype == ALE_FCURVE); const FCurve *fcu = static_cast(ale->data); const float cfra = BKE_scene_frame_get(ac->scene); switch (value_offset_mode) { case KEYFRAME_PASTE_VALUE_OFFSET_CURSOR: { const SpaceGraph *sipo = reinterpret_cast(ac->sl); const float offset = sipo->cursorVal - fcurve_in_copy_buffer.bezt[0].vec[1][1]; return offset; } case KEYFRAME_PASTE_VALUE_OFFSET_CFRA: { const float cfra_y = evaluate_fcurve(fcu, cfra); const float offset = cfra_y - fcurve_in_copy_buffer.bezt[0].vec[1][1]; return offset; } case KEYFRAME_PASTE_VALUE_OFFSET_LEFT_KEY: { bool replace; const int fcu_index = BKE_fcurve_bezt_binarysearch_index( fcu->bezt, cfra, fcu->totvert, &replace); const BezTriple left_key = fcu->bezt[max_ii(fcu_index - 1, 0)]; const float offset = left_key.vec[1][1] - fcurve_in_copy_buffer.bezt[0].vec[1][1]; return offset; } case KEYFRAME_PASTE_VALUE_OFFSET_RIGHT_KEY: { bool replace; const int fcu_index = BKE_fcurve_bezt_binarysearch_index( fcu->bezt, cfra, fcu->totvert, &replace); const BezTriple right_key = fcu->bezt[min_ii(fcu_index, fcu->totvert - 1)]; const float offset = right_key.vec[1][1] - fcurve_in_copy_buffer.bezt[fcurve_in_copy_buffer.totvert - 1].vec[1][1]; return offset; } case KEYFRAME_PASTE_VALUE_OFFSET_NONE: break; } return 0.0f; } eKeyPasteError paste_animedit_keys(bAnimContext *ac, ListBase *anim_data, const KeyframePasteContext &paste_context) { using namespace blender::ed::animation; if (!keyframe_copy_buffer || keyframe_copy_buffer->is_empty()) { return KEYFRAME_PASTE_NOTHING_TO_PASTE; } if (BLI_listbase_is_empty(anim_data)) { return KEYFRAME_PASTE_NOWHERE_TO_PASTE; } const Scene *scene = (ac->scene); const bool from_single = keyframe_copy_buffer->is_single_fcurve(); const bool to_single = BLI_listbase_is_single(anim_data); float offset[2] = {0, 0}; /* methods of offset */ switch (paste_context.offset_mode) { case KEYFRAME_PASTE_OFFSET_CFRA_START: offset[0] = float(scene->r.cfra - keyframe_copy_buffer->first_frame); break; case KEYFRAME_PASTE_OFFSET_CFRA_END: offset[0] = float(scene->r.cfra - keyframe_copy_buffer->last_frame); break; case KEYFRAME_PASTE_OFFSET_CFRA_RELATIVE: offset[0] = float(scene->r.cfra - keyframe_copy_buffer->current_frame); break; case KEYFRAME_PASTE_OFFSET_NONE: offset[0] = 0.0f; break; } if (from_single && to_single) { /* 1:1 match, no tricky checking, just paste. */ bAnimListElem *ale = static_cast(anim_data->first); FCurve *fcu = static_cast(ale->data); /* destination F-Curve */ const FCurve &fcurve_in_copy_buffer = *keyframe_copy_buffer->keyframe_data.channelbag(0)->fcurve(0); offset[1] = paste_get_y_offset( ac, fcurve_in_copy_buffer, ale, paste_context.value_offset_mode); paste_animedit_keys_fcurve( fcu, fcurve_in_copy_buffer, offset, paste_context.merge_mode, false); ale->update |= ANIM_UPDATE_DEFAULT; ANIM_animdata_update(ac, anim_data); return KEYFRAME_PASTE_OK; } /* Try to find "matching" channels to paste keyframes into with increasingly * loose matching heuristics. The process finishes when at least one F-Curve * has been pasted into. */ Vector matchers = { pastebuf_match_path_full, pastebuf_match_path_property, pastebuf_match_index_only}; for (const pastebuf_match_func matcher : matchers) { bool found_match = false; LISTBASE_FOREACH (bAnimListElem *, ale, anim_data) { /* See if there is an F-Curve in the copy buffer that matches this ALE. */ const FCurve *fcurve_in_copy_buffer = pastebuf_find_matching_copybuf_item( matcher, ac->bmain, *ale, from_single, to_single, paste_context); if (!fcurve_in_copy_buffer) { continue; } /* Copy the relevant data from the matching buffer curve. */ offset[1] = paste_get_y_offset( ac, *fcurve_in_copy_buffer, ale, paste_context.value_offset_mode); /* Do the actual pasting. */ FCurve *fcurve_to_paste_into = static_cast(ale->data); ANIM_nla_mapping_apply_if_needed_fcurve(ale, fcurve_to_paste_into, false, false); paste_animedit_keys_fcurve(fcurve_to_paste_into, *fcurve_in_copy_buffer, offset, paste_context.merge_mode, paste_context.flip); ANIM_nla_mapping_apply_if_needed_fcurve(ale, fcurve_to_paste_into, true, false); found_match = true; ale->update |= ANIM_UPDATE_DEFAULT; } /* Don't continue if some fcurves were pasted. */ if (found_match) { break; } } ANIM_animdata_update(ac, anim_data); return KEYFRAME_PASTE_OK; } /** \} */