Files
Brecht Van Lommel 92b555452b Refactor: Logging: Replace more printf with CLOG
Mainly for images, and a few individual cases in animation, ply, UI, and WM.

Pull Request: https://projects.blender.org/blender/blender/pulls/143447
2025-08-11 14:07:45 +02:00

3292 lines
105 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "DNA_action_types.h"
#include "DNA_anim_types.h"
#include "DNA_array_utils.hh"
#include "DNA_defaults.h"
#include "DNA_scene_types.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_base.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_fcurve.hh"
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_nla.hh"
#include "BKE_report.hh"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"
#include "MEM_guardedalloc.h"
#include "BLT_translation.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_action_legacy.hh"
#include "ANIM_animdata.hh"
#include "ANIM_fcurve.hh"
#include "CLG_log.h"
#include "action_runtime.hh"
#include <cstdio>
#include <cstring>
static CLG_LogRef LOG = {"anim.action"};
namespace blender::animrig {
namespace {
/**
* Default identifier for action slots. The first two characters in the identifier indicate the ID
* type of whatever is animated by it.
*
* Since the ID type might not be determined when the slot is created, the prefix starts out at
* XX (see below). Note that no code should use this XX value; use Slot::has_idtype() instead.
*/
constexpr const char *slot_default_name = "Slot";
/**
* Slot identifier prefix for untyped slots (i.e. where `Slot::has_idtype()` returns `false`).
*/
constexpr const char *slot_untyped_prefix = "XX";
constexpr const char *layer_default_name = "Layer";
} // namespace
static animrig::Layer &ActionLayer_alloc()
{
ActionLayer *layer = DNA_struct_default_alloc(ActionLayer);
return layer->wrap();
}
/* Copied from source/blender/blenkernel/intern/grease_pencil.cc.
* Keep an eye on DNA_array_utils.hh; we may want to move these functions in there. */
template<typename T> static void grow_array(T **array, int *num, const int add_num)
{
BLI_assert(add_num > 0);
const int new_array_num = *num + add_num;
T *new_array = MEM_calloc_arrayN<T>(new_array_num, "animrig::action/grow_array");
blender::uninitialized_relocate_n(*array, *num, new_array);
MEM_SAFE_FREE(*array);
*array = new_array;
*num = new_array_num;
}
template<typename T> static void grow_array_and_append(T **array, int *num, T item)
{
grow_array(array, num, 1);
(*array)[*num - 1] = item;
}
template<typename T>
static void grow_array_and_insert(T **array, int *num, const int index, T item)
{
BLI_assert(index >= 0 && index <= *num);
const int new_array_num = *num + 1;
T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);
blender::uninitialized_relocate_n(*array, index, new_array);
new_array[index] = item;
blender::uninitialized_relocate_n(*array + index, *num - index, new_array + index + 1);
MEM_SAFE_FREE(*array);
*array = new_array;
*num = new_array_num;
}
template<typename T> static void shrink_array(T **array, int *num, const int shrink_num)
{
BLI_assert(shrink_num > 0);
const int new_array_num = *num - shrink_num;
if (new_array_num == 0) {
MEM_freeN(*array);
*array = nullptr;
*num = 0;
return;
}
T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);
blender::uninitialized_move_n(*array, new_array_num, new_array);
MEM_freeN(*array);
*array = new_array;
*num = new_array_num;
}
template<typename T> static void shrink_array_and_remove(T **array, int *num, const int index)
{
BLI_assert(index >= 0 && index < *num);
const int new_array_num = *num - 1;
T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);
blender::uninitialized_move_n(*array, index, new_array);
blender::uninitialized_move_n(*array + index + 1, *num - index - 1, new_array + index);
MEM_freeN(*array);
*array = new_array;
*num = new_array_num;
}
/**
* Same as `shrink_array_and_remove()` above, except instead of shifting all the
* elements after the removed item over to fill the gap, it just swaps in the last
* element to where the removed element was.
*/
template<typename T> static void shrink_array_and_swap_remove(T **array, int *num, const int index)
{
BLI_assert(index >= 0 && index < *num);
const int new_array_num = *num - 1;
T *new_array = MEM_calloc_arrayN<T>(new_array_num, __func__);
blender::uninitialized_move_n(*array, index, new_array);
if (index < new_array_num) {
new_array[index] = (*array)[new_array_num];
blender::uninitialized_move_n(*array + index + 1, *num - index - 2, new_array + index + 1);
}
MEM_freeN(*array);
*array = new_array;
*num = new_array_num;
}
/**
* Moves the given (end exclusive) range to index `to`, shifting other items
* before/after to make room.
*
* The range is moved such that the *start* ends up at `to`.
*
* `to` *must* be far away enough from the end of the array for the entire range
* to be moved there without spilling over the end of the array.
*/
template<typename T>
static void array_shift_range(
T *array, const int num, const int range_start, const int range_end, const int to)
{
BLI_assert(range_start <= range_end);
BLI_assert(range_end <= num);
BLI_assert(to <= num + range_start - range_end);
UNUSED_VARS_NDEBUG(num);
if (ELEM(range_start, range_end, to)) {
return;
}
if (to < range_start) {
T *start = array + to;
T *mid = array + range_start;
T *end = array + range_end;
std::rotate(start, mid, end);
}
else {
T *start = array + range_start;
T *mid = array + range_end;
T *end = array + to + range_end - range_start;
std::rotate(start, mid, end);
}
}
/* ----- Action implementation ----------- */
bool Action::is_empty() const
{
/* The check for emptiness has to include the check for an empty `groups` ListBase because of the
* animation filtering code. With the functions `rearrange_action_channels` and
* `join_groups_action_temp` the ownership of FCurves is temporarily transferred to the `groups`
* ListBase leaving `curves` potentially empty. */
return this->layer_array_num == 0 && this->slot_array_num == 0 &&
BLI_listbase_is_empty(&this->curves) && BLI_listbase_is_empty(&this->groups);
}
bool Action::is_action_legacy() const
{
/* This is a valid legacy Action only if there is no layered info. */
return this->layer_array_num == 0 && this->slot_array_num == 0;
}
bool Action::is_action_layered() const
{
/* This is a valid layered Action if there is ANY layered info (because that
* takes precedence) or when there is no legacy info. */
return this->layer_array_num > 0 || this->slot_array_num > 0 ||
(BLI_listbase_is_empty(&this->curves) && BLI_listbase_is_empty(&this->groups));
}
blender::Span<const Layer *> Action::layers() const
{
return blender::Span<const Layer *>{reinterpret_cast<Layer **>(this->layer_array),
this->layer_array_num};
}
blender::Span<Layer *> Action::layers()
{
return blender::Span<Layer *>{reinterpret_cast<Layer **>(this->layer_array),
this->layer_array_num};
}
const Layer *Action::layer(const int64_t index) const
{
return &this->layer_array[index]->wrap();
}
Layer *Action::layer(const int64_t index)
{
return &this->layer_array[index]->wrap();
}
Layer &Action::layer_add(const std::optional<StringRefNull> name)
{
Layer &new_layer = ActionLayer_alloc();
if (name.has_value()) {
STRNCPY_UTF8(new_layer.name, name.value().c_str());
}
else {
STRNCPY_UTF8(new_layer.name, DATA_(layer_default_name));
}
grow_array_and_append<::ActionLayer *>(&this->layer_array, &this->layer_array_num, &new_layer);
this->layer_active_index = this->layer_array_num - 1;
/* If this is the first layer in this Action, it means that it could have been
* used as a legacy Action before. As a result, this->idroot may be non-zero
* while it should be zero for layered Actions.
*
* And since setting this to 0 when it is already supposed to be 0 is fine,
* there is no check for whether this is actually the first layer. */
this->idroot = 0;
return new_layer;
}
static void layer_ptr_destructor(ActionLayer **dna_layer_ptr)
{
Layer &layer = (*dna_layer_ptr)->wrap();
MEM_delete(&layer);
};
bool Action::layer_remove(Layer &layer_to_remove)
{
const int64_t layer_index = this->find_layer_index(layer_to_remove);
if (layer_index < 0) {
return false;
}
dna::array::remove_index(&this->layer_array,
&this->layer_array_num,
&this->layer_active_index,
layer_index,
layer_ptr_destructor);
return true;
}
void Action::layer_keystrip_ensure()
{
/* Ensure a layer. */
Layer *layer;
if (this->layers().is_empty()) {
layer = &this->layer_add(DATA_(layer_default_name));
}
else {
layer = this->layer(0);
}
/* Ensure a keyframe Strip. */
if (layer->strips().is_empty()) {
layer->strip_add(*this, Strip::Type::Keyframe);
}
/* Within the limits of Baklava Phase 1, the above code should not have
* created more than one layer, or more than one strip on the layer. And if a
* layer + strip already existed, that must have been a keyframe strip. */
assert_baklava_phase_1_invariants(*this);
}
int64_t Action::find_layer_index(const Layer &layer) const
{
for (const int64_t layer_index : this->layers().index_range()) {
const Layer *visit_layer = this->layer(layer_index);
if (visit_layer == &layer) {
return layer_index;
}
}
return -1;
}
int64_t Action::find_slot_index(const Slot &slot) const
{
for (const int64_t slot_index : this->slots().index_range()) {
const Slot *visit_slot = this->slot(slot_index);
if (visit_slot == &slot) {
return slot_index;
}
}
return -1;
}
blender::Span<const Slot *> Action::slots() const
{
return blender::Span<Slot *>{reinterpret_cast<Slot **>(this->slot_array), this->slot_array_num};
}
blender::Span<Slot *> Action::slots()
{
return blender::Span<Slot *>{reinterpret_cast<Slot **>(this->slot_array), this->slot_array_num};
}
const Slot *Action::slot(const int64_t index) const
{
return &this->slot_array[index]->wrap();
}
Slot *Action::slot(const int64_t index)
{
return &this->slot_array[index]->wrap();
}
Slot *Action::slot_for_handle(const slot_handle_t handle)
{
const Slot *slot = const_cast<const Action *>(this)->slot_for_handle(handle);
return const_cast<Slot *>(slot);
}
const Slot *Action::slot_for_handle(const slot_handle_t handle) const
{
if (handle == Slot::unassigned) {
return nullptr;
}
/* TODO: implement hash-map lookup. */
for (const Slot *slot : slots()) {
if (slot->handle == handle) {
return slot;
}
}
return nullptr;
}
static void slot_identifier_ensure_unique(Action &action, Slot &slot)
{
auto check_name_is_used = [&](const StringRef name) -> bool {
for (const Slot *slot_iter : action.slots()) {
if (slot_iter == &slot) {
/* Don't compare against the slot that's being renamed. */
continue;
}
if (slot_iter->identifier == name) {
return true;
}
}
return false;
};
BLI_uniquename_cb(check_name_is_used, "", '.', slot.identifier, sizeof(slot.identifier));
}
void Action::slot_display_name_set(Main &bmain, Slot &slot, StringRefNull new_display_name)
{
this->slot_display_name_define(slot, new_display_name);
this->slot_identifier_propagate(bmain, slot);
}
void Action::slot_display_name_define(Slot &slot, StringRefNull new_display_name)
{
BLI_assert_msg(StringRef(new_display_name).size() >= 1,
"Action Slot display names must not be empty");
BLI_assert_msg(StringRef(slot.identifier).size() >= 2,
"Action Slot's existing identifier lacks the two-character type prefix, which "
"would make the display name copy meaningless due to early null termination.");
BLI_strncpy_utf8(slot.identifier + 2, new_display_name.c_str(), ARRAY_SIZE(slot.identifier) - 2);
slot_identifier_ensure_unique(*this, slot);
}
void Action::slot_idtype_define(Slot &slot, ID_Type idtype)
{
slot.idtype = idtype;
slot.identifier_ensure_prefix();
slot_identifier_ensure_unique(*this, slot);
}
void Action::slot_identifier_set(Main &bmain, Slot &slot, const StringRefNull new_identifier)
{
/* TODO: maybe this function should only set the 'identifier without prefix' aka the 'display
* name'. That way only `this->id_type` is responsible for the prefix. I (Sybren) think that's
* easier to determine when the code is a bit more mature, and we can see what the majority of
* the calls to this function actually do/need. */
this->slot_identifier_define(slot, new_identifier);
this->slot_identifier_propagate(bmain, slot);
}
void Action::slot_identifier_define(Slot &slot, const StringRefNull new_identifier)
{
BLI_assert_msg(
StringRef(new_identifier).size() >= Slot::identifier_length_min,
"Action Slot identifiers must be large enough for a 2-letter ID code + the display name");
STRNCPY_UTF8(slot.identifier, new_identifier.c_str());
slot_identifier_ensure_unique(*this, slot);
}
void Action::slot_identifier_propagate(Main &bmain, const Slot &slot)
{
/* Just loop over all animatable IDs in the main database. */
ListBase *lb;
ID *id;
FOREACH_MAIN_LISTBASE_BEGIN (&bmain, lb) {
FOREACH_MAIN_LISTBASE_ID_BEGIN (lb, id) {
if (!id_can_have_animdata(id)) {
/* This ID type cannot have any animation, so ignore all and continue to
* the next ID type. */
break;
}
AnimData *adt = BKE_animdata_from_id(id);
if (!adt || adt->action != this) {
/* Not animated by this Action. */
continue;
}
if (adt->slot_handle != slot.handle) {
/* Not animated by this Slot. */
continue;
}
/* Ensure the Slot identifier on the AnimData is correct. */
STRNCPY_UTF8(adt->last_slot_identifier, slot.identifier);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
FOREACH_MAIN_LISTBASE_END;
}
Slot *Action::slot_find_by_identifier(const StringRefNull slot_identifier)
{
for (Slot *slot : slots()) {
if (STREQ(slot->identifier, slot_identifier.c_str())) {
return slot;
}
}
return nullptr;
}
Slot &Action::slot_allocate()
{
Slot &slot = *MEM_new<Slot>(__func__);
this->last_slot_handle++;
BLI_assert_msg(this->last_slot_handle > 0, "Action Slot handle overflow");
slot.handle = this->last_slot_handle;
/* Set the default flags. These cannot be set via the 'DNA defaults' system,
* as that would require knowing which bit corresponds with which flag. That's
* only known to the C++ wrapper code. */
slot.set_expanded(true);
return slot;
}
Slot &Action::slot_add()
{
Slot &slot = this->slot_allocate();
/* Assign the default name and the 'untyped' identifier prefix. */
STRNCPY_UTF8(slot.identifier, slot_untyped_prefix);
BLI_strncpy_utf8(slot.identifier + 2, DATA_(slot_default_name), ARRAY_SIZE(slot.identifier) - 2);
/* Append the Slot to the Action. */
grow_array_and_append<::ActionSlot *>(&this->slot_array, &this->slot_array_num, &slot);
slot_identifier_ensure_unique(*this, slot);
/* If this is the first slot in this Action, it means that it could have
* been used as a legacy Action before. As a result, this->idroot may be
* non-zero while it should be zero for layered Actions.
*
* And since setting this to 0 when it is already supposed to be 0 is fine,
* there is no check for whether this is actually the first layer. */
this->idroot = 0;
return slot;
}
Slot &Action::slot_add_for_id_type(const ID_Type idtype)
{
Slot &slot = this->slot_add();
slot.idtype = idtype;
slot.identifier_ensure_prefix();
BLI_strncpy_utf8(slot.identifier + 2, DATA_(slot_default_name), ARRAY_SIZE(slot.identifier) - 2);
slot_identifier_ensure_unique(*this, slot);
/* No need to call anim.slot_identifier_propagate() as nothing will be using
* this brand new Slot yet. */
return slot;
}
Slot &Action::slot_add_for_id(const ID &animated_id)
{
Slot &slot = this->slot_add();
slot.idtype = GS(animated_id.name);
/* Determine the identifier for this slot, prioritizing transparent
* auto-selection when toggling between Actions. That's why the last-used slot
* identifier is used here, and the ID name only as fallback. */
const AnimData *adt = BKE_animdata_from_id(&animated_id);
const StringRefNull last_slot_identifier = adt ? adt->last_slot_identifier : "";
StringRefNull slot_identifier = last_slot_identifier;
if (slot_identifier.is_empty()) {
slot_identifier = animated_id.name;
}
this->slot_identifier_define(slot, slot_identifier);
/* No need to call anim.slot_identifier_propagate() as nothing will be using
* this brand new Slot yet. */
/* The last-used slot might have had a different ID type through some quirk (changes to linked
* data, for example). So better ensure that the identifier prefix is correct on this new slot,
* instead of relying for 100% on the old one. */
slot.identifier_ensure_prefix();
return slot;
}
static void slot_ptr_destructor(ActionSlot **dna_slot_ptr)
{
Slot &slot = (*dna_slot_ptr)->wrap();
MEM_delete(&slot);
};
bool Action::slot_remove(Slot &slot_to_remove)
{
/* Check that this slot belongs to this Action. */
const int64_t slot_index = this->find_slot_index(slot_to_remove);
if (slot_index < 0) {
return false;
}
/* Remove the slot's data from each keyframe strip. */
for (StripKeyframeData *strip_data : this->strip_keyframe_data()) {
strip_data->slot_data_remove(slot_to_remove.handle);
}
/* Don't bother un-assigning this slot from its users. The slot handle will
* not be reused by a new slot anyway. */
/* Remove the actual slot. */
dna::array::remove_index(
&this->slot_array, &this->slot_array_num, nullptr, slot_index, slot_ptr_destructor);
return true;
}
void Action::slot_move_to_index(Slot &slot, const int to_slot_index)
{
BLI_assert(this->slots().index_range().contains(to_slot_index));
const int from_slot_index = this->slots().first_index_try(&slot);
BLI_assert_msg(from_slot_index >= 0, "Slot not in this action.");
array_shift_range(
this->slot_array, this->slot_array_num, from_slot_index, from_slot_index + 1, to_slot_index);
}
void Action::slot_active_set(const slot_handle_t slot_handle)
{
for (Slot *slot : slots()) {
slot->set_active(slot->handle == slot_handle);
}
}
Slot *Action::slot_active_get()
{
for (Slot *slot : slots()) {
if (slot->is_active()) {
return slot;
}
}
return nullptr;
}
bool Action::is_slot_animated(const slot_handle_t slot_handle) const
{
if (slot_handle == Slot::unassigned) {
return false;
}
Span<const FCurve *> fcurves = fcurves_for_action_slot(*this, slot_handle);
return !fcurves.is_empty();
}
int Action::strip_keyframe_data_append(StripKeyframeData *strip_data)
{
BLI_assert(strip_data != nullptr);
grow_array_and_append<ActionStripKeyframeData *>(
&this->strip_keyframe_data_array, &this->strip_keyframe_data_array_num, strip_data);
return this->strip_keyframe_data_array_num - 1;
}
void Action::strip_keyframe_data_remove_if_unused(const int index)
{
BLI_assert(index >= 0 && index < this->strip_keyframe_data_array_num);
/* Make sure the data isn't being used anywhere. */
for (const Layer *layer : this->layers()) {
for (const Strip *strip : layer->strips()) {
if (strip->type() == Strip::Type::Keyframe && strip->data_index == index) {
return;
}
}
}
/* Free the item to be removed. */
MEM_delete<StripKeyframeData>(
static_cast<StripKeyframeData *>(this->strip_keyframe_data_array[index]));
/* Remove the item, swapping in the item at the end of the array. */
shrink_array_and_swap_remove<ActionStripKeyframeData *>(
&this->strip_keyframe_data_array, &this->strip_keyframe_data_array_num, index);
/* Update strips that pointed at the swapped-in item.
*
* Note that we don't special-case the corner-case where the removed data was
* at the end of the array, but it ends up not mattering because then
* `old_index == index`. */
const int old_index = this->strip_keyframe_data_array_num;
for (Layer *layer : this->layers()) {
for (Strip *strip : layer->strips()) {
if (strip->type() == Strip::Type::Keyframe && strip->data_index == old_index) {
strip->data_index = index;
}
}
}
}
Span<const StripKeyframeData *> Action::strip_keyframe_data() const
{
/* The reinterpret cast is needed because `strip_keyframe_data_array` is for
* pointers to the C type `ActionStripKeyframeData`, but we want the C++
* wrapper type `StripKeyframeData`. */
return Span<StripKeyframeData *>{
reinterpret_cast<StripKeyframeData **>(this->strip_keyframe_data_array),
this->strip_keyframe_data_array_num};
}
Span<StripKeyframeData *> Action::strip_keyframe_data()
{
/* The reinterpret cast is needed because `strip_keyframe_data_array` is for
* pointers to the C type `ActionStripKeyframeData`, but we want the C++
* wrapper type `StripKeyframeData`. */
return Span<StripKeyframeData *>{
reinterpret_cast<StripKeyframeData **>(this->strip_keyframe_data_array),
this->strip_keyframe_data_array_num};
}
Layer *Action::get_layer_for_keyframing()
{
assert_baklava_phase_1_invariants(*this);
if (this->layers().is_empty()) {
return nullptr;
}
return this->layer(0);
}
void Action::slot_identifier_ensure_prefix(Slot &slot)
{
slot.identifier_ensure_prefix();
slot_identifier_ensure_unique(*this, slot);
}
void Action::slot_setup_for_id(Slot &slot, const ID &animated_id)
{
if (!ID_IS_EDITABLE(this) || ID_IS_OVERRIDE_LIBRARY(this)) {
/* Do not write to linked data. For now, also avoid changing the slot identifier on an
* override. Actions cannot have library overrides at the moment, and when they do, this should
* actually get designed. For now, it's better to avoid editing data than editing too much. */
return;
}
if (slot.has_idtype()) {
BLI_assert(slot.idtype == GS(animated_id.name));
return;
}
slot.idtype = GS(animated_id.name);
this->slot_identifier_ensure_prefix(slot);
}
bool Action::has_keyframes(const slot_handle_t action_slot_handle) const
{
if (this->is_action_legacy()) {
/* Old BKE_action_has_motion(const bAction *act) implementation. */
LISTBASE_FOREACH (const FCurve *, fcu, &this->curves) {
if (fcu->totvert) {
return true;
}
}
return false;
}
for (const FCurve *fcu : fcurves_for_action_slot(*this, action_slot_handle)) {
if (fcu->totvert) {
return true;
}
}
return false;
}
bool Action::has_single_frame() const
{
bool found_key = false;
float found_key_frame = 0.0f;
for (const FCurve *fcu : legacy::fcurves_all(this)) {
switch (fcu->totvert) {
case 0:
/* No keys, so impossible to come to a conclusion on this curve alone. */
continue;
case 1:
/* Single key, which is the complex case, so handle below. */
break;
default:
/* Multiple keys, so there is animation. */
return false;
}
const float this_key_frame = fcu->bezt != nullptr ? fcu->bezt[0].vec[1][0] :
fcu->fpt[0].vec[0];
if (!found_key) {
found_key = true;
found_key_frame = this_key_frame;
continue;
}
/* The graph editor rounds to 1/1000th of a frame, so it's not necessary to be really precise
* with these comparisons. */
if (!compare_ff(found_key_frame, this_key_frame, 0.001f)) {
/* This key differs from the already-found key, so this Action represents animation. */
return false;
}
}
/* There is only a single frame if we found at least one key. */
return found_key;
}
bool Action::is_cyclic() const
{
return (this->flag & ACT_FRAME_RANGE) && (this->flag & ACT_CYCLIC);
}
/** Return the frame range of the span of keys. */
static float2 get_frame_range_of_fcurves(Span<const FCurve *> fcurves, bool include_modifiers);
float2 Action::get_frame_range() const
{
if (this->flag & ACT_FRAME_RANGE) {
return {this->frame_start, this->frame_end};
}
Vector<const FCurve *> all_fcurves = legacy::fcurves_all(this);
return get_frame_range_of_fcurves(all_fcurves, false);
}
float2 Action::get_frame_range_of_slot(const slot_handle_t slot_handle) const
{
if (this->flag & ACT_FRAME_RANGE) {
return {this->frame_start, this->frame_end};
}
Vector<const FCurve *> legacy_fcurves;
Span<const FCurve *> fcurves_to_consider;
if (this->is_action_layered()) {
fcurves_to_consider = fcurves_for_action_slot(*this, slot_handle);
}
else {
legacy_fcurves = legacy::fcurves_all(this);
fcurves_to_consider = legacy_fcurves;
}
return get_frame_range_of_fcurves(fcurves_to_consider, false);
}
float2 Action::get_frame_range_of_keys(const bool include_modifiers) const
{
return get_frame_range_of_fcurves(legacy::fcurves_all(this), include_modifiers);
}
static float2 get_frame_range_of_fcurves(Span<const FCurve *> fcurves,
const bool include_modifiers)
{
float min = 999999999.0f, max = -999999999.0f;
bool foundvert = false, foundmod = false;
for (const FCurve *fcu : fcurves) {
/* if curve has keyframes, consider them first */
if (fcu->totvert) {
float nmin, nmax;
/* get extents for this curve
* - no "selected only", since this is often used in the backend
* - no "minimum length" (we will apply this later), otherwise
* single-keyframe curves will increase the overall length by
* a phantom frame (#50354)
*/
BKE_fcurve_calc_range(fcu, &nmin, &nmax, false);
/* compare to the running tally */
min = min_ff(min, nmin);
max = max_ff(max, nmax);
foundvert = true;
}
/* if include_modifiers is enabled, need to consider modifiers too
* - only really care about the last modifier
*/
if ((include_modifiers) && (fcu->modifiers.last)) {
FModifier *fcm = static_cast<FModifier *>(fcu->modifiers.last);
/* only use the maximum sensible limits of the modifiers if they are more extreme */
switch (fcm->type) {
case FMODIFIER_TYPE_LIMITS: /* Limits F-Modifier */
{
FMod_Limits *fmd = static_cast<FMod_Limits *>(fcm->data);
if (fmd->flag & FCM_LIMIT_XMIN) {
min = min_ff(min, fmd->rect.xmin);
}
if (fmd->flag & FCM_LIMIT_XMAX) {
max = max_ff(max, fmd->rect.xmax);
}
break;
}
case FMODIFIER_TYPE_CYCLES: /* Cycles F-Modifier */
{
FMod_Cycles *fmd = static_cast<FMod_Cycles *>(fcm->data);
if (fmd->before_mode != FCM_EXTRAPOLATE_NONE) {
min = MINAFRAMEF;
}
if (fmd->after_mode != FCM_EXTRAPOLATE_NONE) {
max = MAXFRAMEF;
}
break;
}
/* TODO: function modifier may need some special limits */
default: /* all other standard modifiers are on the infinite range... */
min = MINAFRAMEF;
max = MAXFRAMEF;
break;
}
foundmod = true;
}
}
if (foundvert || foundmod) {
return float2{max_ff(min, MINAFRAMEF), min_ff(max, MAXFRAMEF)};
}
return float2{0.0f, 0.0f};
}
/* ----- ActionLayer implementation ----------- */
Layer *Layer::duplicate_with_shallow_strip_copies(const StringRefNull allocation_name) const
{
ActionLayer *copy = MEM_callocN<ActionLayer>(allocation_name.c_str());
*copy = *reinterpret_cast<const ActionLayer *>(this);
/* Make a shallow copy of the Strips, without copying their data. */
copy->strip_array = MEM_calloc_arrayN<ActionStrip *>(this->strip_array_num,
allocation_name.c_str());
for (int i : this->strips().index_range()) {
Strip *strip_copy = MEM_new<Strip>(allocation_name.c_str(), *this->strip(i));
copy->strip_array[i] = strip_copy;
}
return &copy->wrap();
}
Layer::~Layer()
{
for (Strip *strip : this->strips()) {
MEM_delete(strip);
}
MEM_SAFE_FREE(this->strip_array);
this->strip_array_num = 0;
}
blender::Span<const Strip *> Layer::strips() const
{
return blender::Span<Strip *>{reinterpret_cast<Strip **>(this->strip_array),
this->strip_array_num};
}
blender::Span<Strip *> Layer::strips()
{
return blender::Span<Strip *>{reinterpret_cast<Strip **>(this->strip_array),
this->strip_array_num};
}
const Strip *Layer::strip(const int64_t index) const
{
return &this->strip_array[index]->wrap();
}
Strip *Layer::strip(const int64_t index)
{
return &this->strip_array[index]->wrap();
}
Strip &Layer::strip_add(Action &owning_action, const Strip::Type strip_type)
{
Strip &strip = Strip::create(owning_action, strip_type);
/* Add the new strip to the strip array. */
grow_array_and_append<::ActionStrip *>(&this->strip_array, &this->strip_array_num, &strip);
return strip;
}
static void strip_ptr_destructor(ActionStrip **dna_strip_ptr)
{
Strip &strip = (*dna_strip_ptr)->wrap();
MEM_delete(&strip);
};
bool Layer::strip_remove(Action &owning_action, Strip &strip)
{
const int64_t strip_index = this->find_strip_index(strip);
if (strip_index < 0) {
return false;
}
const Strip::Type strip_type = strip.type();
const int data_index = strip.data_index;
dna::array::remove_index(
&this->strip_array, &this->strip_array_num, nullptr, strip_index, strip_ptr_destructor);
/* It's important that we do this *after* removing the strip itself
* (immediately above), because otherwise the strip will be found as a
* still-existing user of the strip data and thus the strip data won't be
* removed even if this strip was the last user. */
switch (strip_type) {
case Strip::Type::Keyframe:
owning_action.strip_keyframe_data_remove_if_unused(data_index);
break;
}
return true;
}
int64_t Layer::find_strip_index(const Strip &strip) const
{
for (const int64_t strip_index : this->strips().index_range()) {
const Strip *visit_strip = this->strip(strip_index);
if (visit_strip == &strip) {
return strip_index;
}
}
return -1;
}
/* ----- ActionSlot implementation ----------- */
Slot::Slot()
{
/* Zero-initialize the DNA struct. 'this' is a C++ class, and shouldn't be memset like this. */
memset(static_cast<ActionSlot *>(this), 0, sizeof(ActionSlot));
this->runtime = MEM_new<SlotRuntime>(__func__);
}
Slot::Slot(const Slot &other) : ActionSlot(other)
{
this->runtime = MEM_new<SlotRuntime>(__func__);
}
Slot::~Slot()
{
MEM_delete(this->runtime);
}
void Slot::blend_read_post()
{
BLI_assert(!this->runtime);
this->runtime = MEM_new<SlotRuntime>(__func__);
}
bool Slot::is_suitable_for(const ID &animated_id) const
{
if (!this->has_idtype()) {
/* Without specific ID type set, this Slot can animate any ID. */
return true;
}
/* Check that the ID type is compatible with this slot. */
const int animated_idtype = GS(animated_id.name);
return this->idtype == animated_idtype;
}
bool Slot::has_idtype() const
{
return this->idtype != 0;
}
Slot::Flags Slot::flags() const
{
return static_cast<Slot::Flags>(this->slot_flags);
}
bool Slot::is_expanded() const
{
return this->slot_flags & uint8_t(Flags::Expanded);
}
void Slot::set_expanded(const bool expanded)
{
if (expanded) {
this->slot_flags |= uint8_t(Flags::Expanded);
}
else {
this->slot_flags &= ~uint8_t(Flags::Expanded);
}
}
bool Slot::is_selected() const
{
return this->slot_flags & uint8_t(Flags::Selected);
}
void Slot::set_selected(const bool selected)
{
if (selected) {
this->slot_flags |= uint8_t(Flags::Selected);
}
else {
this->slot_flags &= ~uint8_t(Flags::Selected);
}
}
bool Slot::is_active() const
{
return this->slot_flags & uint8_t(Flags::Active);
}
void Slot::set_active(const bool active)
{
if (active) {
this->slot_flags |= uint8_t(Flags::Active);
}
else {
this->slot_flags &= ~uint8_t(Flags::Active);
}
}
Span<ID *> Slot::users(Main &bmain) const
{
if (bmain.is_action_slot_to_id_map_dirty) {
internal::rebuild_slot_user_cache(bmain);
}
BLI_assert(this->runtime);
return this->runtime->users.as_span();
}
Vector<ID *> Slot::runtime_users()
{
BLI_assert_msg(this->runtime, "Slot::runtime should always be allocated");
return this->runtime->users;
}
void Slot::users_add(ID &animated_id)
{
BLI_assert(this->runtime);
this->runtime->users.append_non_duplicates(&animated_id);
}
void Slot::users_remove(ID &animated_id)
{
BLI_assert(this->runtime);
Vector<ID *> &users = this->runtime->users;
/* Even though users_add() ensures that there are no duplicates, there's still things like
* pointer swapping etc. that can happen via the foreach-id looping code. That means that the
* entries in the user map are not 100% under control of the user_add() and user_remove()
* function, and thus we cannot assume that there are no duplicates. */
users.remove_if([&](const ID *user) { return user == &animated_id; });
}
void Slot::users_invalidate(Main &bmain)
{
bmain.is_action_slot_to_id_map_dirty = true;
}
std::string Slot::idtype_string() const
{
if (!this->has_idtype()) {
return slot_untyped_prefix;
}
char name[3] = {0};
*reinterpret_cast<short *>(name) = this->idtype;
return name;
}
StringRef Slot::identifier_prefix() const
{
StringRef identifier(this->identifier);
BLI_assert(identifier.size() >= 2);
return identifier.substr(0, 2);
}
StringRefNull Slot::identifier_without_prefix() const
{
BLI_assert(StringRef(this->identifier).size() >= identifier_length_min);
/* Avoid accessing an uninitialized part of the string accidentally. */
if (this->identifier[0] == '\0' || this->identifier[1] == '\0') {
return "";
}
return this->identifier + 2;
}
void Slot::identifier_ensure_prefix()
{
BLI_assert(StringRef(this->identifier).size() >= identifier_length_min);
if (StringRef(this->identifier).size() < 2) {
/* The code below would overwrite the trailing 0-byte. */
this->identifier[2] = '\0';
}
if (!this->has_idtype()) {
/* A zero idtype is not going to convert to a two-character string, so we
* need to explicitly assign the default prefix. */
this->identifier[0] = slot_untyped_prefix[0];
this->identifier[1] = slot_untyped_prefix[1];
return;
}
*reinterpret_cast<short *>(this->identifier) = this->idtype;
}
/* ----- Functions ----------- */
Action &action_add(Main &bmain, const StringRefNull name)
{
bAction *dna_action = BKE_action_add(&bmain, name.c_str());
id_us_clear_real(&dna_action->id);
return dna_action->wrap();
}
bool assign_action(bAction *action, ID &animated_id)
{
AnimData *adt = BKE_animdata_ensure_id(&animated_id);
if (!adt) {
return false;
}
return assign_action(action, {animated_id, *adt});
}
bool assign_action(bAction *action, const OwnedAnimData owned_adt)
{
if (!BKE_animdata_action_editable(&owned_adt.adt)) {
/* Cannot remove, otherwise things turn to custard. */
BKE_report(nullptr, RPT_ERROR, "Cannot change action, as it is still being edited in NLA");
return false;
}
return generic_assign_action(owned_adt.owner_id,
action,
owned_adt.adt.action,
owned_adt.adt.slot_handle,
owned_adt.adt.last_slot_identifier);
}
bool assign_tmpaction(bAction *action, const OwnedAnimData owned_adt)
{
return generic_assign_action(owned_adt.owner_id,
action,
owned_adt.adt.tmpact,
owned_adt.adt.tmp_slot_handle,
owned_adt.adt.tmp_last_slot_identifier);
}
bool unassign_action(ID &animated_id)
{
return assign_action(nullptr, animated_id);
}
bool unassign_action(OwnedAnimData owned_adt)
{
return assign_action(nullptr, owned_adt);
}
Slot *assign_action_ensure_slot_for_keying(Action &action, ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
Slot *slot;
/* Find a suitable slot, but be stricter about when to allow searching by name
* than generic_slot_for_autoassign(...). */
if (adt && adt->action == &action) {
/* The slot handle is only valid when this action is already assigned.
* Otherwise it's meaningless. */
slot = action.slot_for_handle(adt->slot_handle);
/* If this Action is already assigned, a search by name is inappropriate, as it might
* re-assign an intentionally-unassigned slot. */
}
else {
/* In this case a by-name search is ok, so defer to generic_slot_for_autoassign(). */
slot = generic_slot_for_autoassign(animated_id, action, adt ? adt->last_slot_identifier : "");
}
/* As a last resort, if there is only one slot and it has no ID type yet, use that. This is what
* gets created for the backwards compatibility RNA API, for example to allow
* `action.fcurves.new()`. Key insertion should use that slot as well. */
if (!slot && action.slots().size() == 1) {
Slot *first_slot = action.slot(0);
if (!first_slot->has_idtype()) {
slot = first_slot;
}
}
/* If no suitable slot was found, create a new one. */
if (!slot || !slot->is_suitable_for(animated_id)) {
slot = &action.slot_add_for_id(animated_id);
}
/* Only try to assign the Action to the ID if it is not already assigned.
* Assignment can fail when the ID is in NLA Tweak mode. */
const bool is_correct_action = adt && adt->action == &action;
if (!is_correct_action && !assign_action(&action, animated_id)) {
return nullptr;
}
const bool is_correct_slot = adt && adt->slot_handle == slot->handle;
if (!is_correct_slot && assign_action_slot(slot, animated_id) != ActionSlotAssignmentResult::OK)
{
/* This should never happen, as a few lines above a new slot is created for
* this ID if the found one wasn't deemed suitable. */
BLI_assert_unreachable();
return nullptr;
}
return slot;
}
static bool is_id_using_action_slot(const ID &animated_id,
const Action &action,
const slot_handle_t slot_handle)
{
auto visit_action_use = [&](const Action &used_action, slot_handle_t used_slot_handle) -> bool {
const bool is_used = (&used_action == &action && used_slot_handle == slot_handle);
return !is_used; /* Stop searching when we found a use of this Action+Slot. */
};
const bool looped_until_end = foreach_action_slot_use(animated_id, visit_action_use);
return !looped_until_end;
}
bool generic_assign_action(ID &animated_id,
bAction *action_to_assign,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *slot_identifier)
{
BLI_assert(slot_identifier);
if (action_to_assign && legacy::action_treat_as_legacy(*action_to_assign)) {
/* Check that the Action is suitable for this ID type.
* This is only necessary for legacy Actions. */
if (!BKE_animdata_action_ensure_idroot(&animated_id, action_to_assign)) {
BKE_reportf(
nullptr,
RPT_ERROR,
"Could not set action '%s' to animate ID '%s', as it does not have suitably rooted "
"paths for this purpose",
action_to_assign->id.name + 2,
animated_id.name);
return false;
}
}
/* Un-assign any previously-assigned Action first. */
if (action_ptr_ref) {
/* Un-assign the slot. This will always succeed, so no need to check the result. */
if (slot_handle_ref != Slot::unassigned) {
const ActionSlotAssignmentResult result = generic_assign_action_slot(
nullptr, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
BLI_assert(result == ActionSlotAssignmentResult::OK);
UNUSED_VARS_NDEBUG(result);
}
/* Un-assign the Action itself. */
id_us_min(&action_ptr_ref->id);
action_ptr_ref = nullptr;
}
if (!action_to_assign) {
/* Un-assigning was the point, so the work is done. */
return true;
}
/* Assign the new Action. */
action_ptr_ref = action_to_assign;
id_us_plus(&action_ptr_ref->id);
/* Auto-assign a slot. */
Slot *slot = generic_slot_for_autoassign(animated_id, action_ptr_ref->wrap(), slot_identifier);
const ActionSlotAssignmentResult result = generic_assign_action_slot(
slot, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
BLI_assert(result == ActionSlotAssignmentResult::OK);
UNUSED_VARS_NDEBUG(result);
return true;
}
Slot *generic_slot_for_autoassign(const ID &animated_id,
Action &action,
const StringRefNull last_slot_identifier)
{
/* The slot-finding code in assign_action_ensure_slot_for_keying() is very
* similar to the code here (differences are documented there). It is very
* likely that changes in the logic here should be applied there as well. */
/* Try the slot identifier, if it is set. */
if (!last_slot_identifier.is_empty()) {
/* If the last-used slot identifier was 'untyped', i.e. started with XX, see if something more
* specific to this ID type exists.
*
* If there is any choice in the matter, the more specific slot is chosen. In other words, in
* this case:
*
* - last_slot_identifier = `XXSlot`
* - both `XXSlot` and `OBSlot` exist on the Action (where `OB` represents the ID type of
* `animated_id`).
*
* the `OBSlot` should be chosen. This means that `XXSlot` NOT being auto-assigned if there is
* an alternative. Since untyped slots are bound on assignment, this design keeps the Action
* as-is, which means that the `XXSlot` remains untyped and thus the user is free to assign
* this to another ID type if desired. */
const bool last_used_identifier_is_typed = last_slot_identifier.substr(0, 2) !=
slot_untyped_prefix;
if (!last_used_identifier_is_typed) {
const std::string with_idtype_prefix = StringRef(animated_id.name, 2) +
last_slot_identifier.substr(2);
Slot *slot = action.slot_find_by_identifier(with_idtype_prefix);
if (slot && slot->is_suitable_for(animated_id)) {
return slot;
}
}
/* See if the actual last-used slot identifier can be matched. */
Slot *slot = action.slot_find_by_identifier(last_slot_identifier);
if (slot && slot->is_suitable_for(animated_id)) {
return slot;
}
/* If the last-used slot identifier was IDSomething, and XXSomething exists (where ID = the
* ID code of the animated ID), fall back to the XX. If slot `IDSomething` existed, the code
* above would have already returned it. */
if (last_used_identifier_is_typed) {
const std::string with_untyped_prefix = StringRef(slot_untyped_prefix) +
last_slot_identifier.substr(2);
Slot *slot = action.slot_find_by_identifier(with_untyped_prefix);
if (slot && slot->is_suitable_for(animated_id)) {
return slot;
}
}
}
/* Search for the ID name (which includes the ID type). */
{
Slot *slot = action.slot_find_by_identifier(animated_id.name);
if (slot && slot->is_suitable_for(animated_id)) {
return slot;
}
}
/* If there is only one slot, and it is not specific to any ID type, use that.
*
* This should only trigger in some special cases, like legacy Actions that were converted to
* slotted Actions by the versioning code, where the legacy Action was never assigned to anything
* (and thus had idroot = 0).
*
* This might seem overly specific, and for convenience of automatically auto-assigning a slot,
* it might be tempting to remove the "slot->has_idtype()" check. However, that would make the
* following workflow significantly more cumbersome:
*
* - Animate `Cube`. This creates `CubeAction` with a single slot `OBCube`.
* - Assign `CubeAction` to `Suzanne`, with the intent of animating both `Cube` and `Suzanne`
* with the same Action.
* - This should **not** auto-assign the `OBCube` slot to `Suzanne`, as that will overwrite any
* property of `Suzanne` with the animated values for the `OBCube` slot.
*
* Recovering from this will be hard, as an undo will revert both the overwriting of properties
* and the assignment of the Action. */
if (action.slots().size() == 1) {
Slot *slot = action.slot(0);
if (!slot->has_idtype()) {
return slot;
}
}
return nullptr;
}
ActionSlotAssignmentResult generic_assign_action_slot(Slot *slot_to_assign,
ID &animated_id,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *slot_identifier)
{
BLI_assert(slot_identifier);
if (!action_ptr_ref) {
/* No action assigned yet, so no way to assign a slot. */
return ActionSlotAssignmentResult::MissingAction;
}
Action &action = action_ptr_ref->wrap();
/* Check that the slot can actually be assigned. */
if (slot_to_assign) {
if (!action.slots().contains(slot_to_assign)) {
return ActionSlotAssignmentResult::SlotNotFromAction;
}
if (!slot_to_assign->is_suitable_for(animated_id)) {
return ActionSlotAssignmentResult::SlotNotSuitable;
}
}
Slot *slot_to_unassign = action.slot_for_handle(slot_handle_ref);
/* If there was a previously-assigned slot, unassign it first. */
slot_handle_ref = Slot::unassigned;
if (slot_to_unassign) {
/* Make sure that the stored Slot identifier is up to date. The slot identifier might have
* changed in a way that wasn't copied into the ADT yet (for example when the
* Action is linked from another file), so better copy the identifier to be sure
* that it can be transparently reassigned later.
*
* TODO: Replace this with a BLI_assert() that the identifier is as expected, and "simply"
* ensure this identifier is always correct. */
BLI_strncpy_utf8(slot_identifier, slot_to_unassign->identifier, Slot::identifier_length_max);
/* If this was the last use of this slot, remove this ID from its users. */
if (!is_id_using_action_slot(animated_id, action, slot_to_unassign->handle)) {
slot_to_unassign->users_remove(animated_id);
}
}
if (!slot_to_assign) {
return ActionSlotAssignmentResult::OK;
}
action.slot_setup_for_id(*slot_to_assign, animated_id);
slot_handle_ref = slot_to_assign->handle;
BLI_strncpy_utf8(slot_identifier, slot_to_assign->identifier, Slot::identifier_length_max);
slot_to_assign->users_add(animated_id);
return ActionSlotAssignmentResult::OK;
}
ActionSlotAssignmentResult generic_assign_action_slot_handle(slot_handle_t slot_handle_to_assign,
ID &animated_id,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *slot_identifier)
{
if (slot_handle_to_assign == Slot::unassigned && !action_ptr_ref) {
/* No Action assigned, so no slot was used anyway. Just blindly assign the
* 'unassigned' handle. */
slot_handle_ref = Slot::unassigned;
return ActionSlotAssignmentResult::OK;
}
if (!action_ptr_ref) {
/* No Action to verify the slot handle is valid. As the slot handle will be
* completely ignored when re-assigning an Action, better to refuse setting
* it altogether. This will make bugs more obvious. */
return ActionSlotAssignmentResult::MissingAction;
}
Slot *slot = action_ptr_ref->wrap().slot_for_handle(slot_handle_to_assign);
return generic_assign_action_slot(
slot, animated_id, action_ptr_ref, slot_handle_ref, slot_identifier);
}
bool is_action_assignable_to(const bAction *dna_action, const ID_Type id_code)
{
if (!dna_action) {
/* Clearing the Action is always possible. */
return true;
}
if (dna_action->idroot == 0) {
/* This is either a never-assigned legacy action, or a layered action. In
* any case, it can be assigned to any ID. */
return true;
}
const animrig::Action &action = dna_action->wrap();
if (legacy::action_treat_as_legacy(action)) {
/* Legacy Actions can only be assigned if their idroot matches. Empty
* Actions are considered both 'layered' and 'legacy' at the same time,
* hence this condition checks for 'not layered' rather than 'legacy'. */
return action.idroot == id_code;
}
return true;
}
ActionSlotAssignmentResult assign_action_slot(Slot *slot_to_assign, ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (!adt) {
return ActionSlotAssignmentResult::MissingAction;
}
return generic_assign_action_slot(
slot_to_assign, animated_id, adt->action, adt->slot_handle, adt->last_slot_identifier);
}
ActionSlotAssignmentResult assign_action_and_slot(Action *action,
Slot *slot_to_assign,
ID &animated_id)
{
if (!assign_action(action, animated_id)) {
return ActionSlotAssignmentResult::MissingAction;
}
return assign_action_slot(slot_to_assign, animated_id);
}
ActionSlotAssignmentResult assign_tmpaction_and_slot_handle(bAction *action,
const slot_handle_t slot_handle,
const OwnedAnimData owned_adt)
{
if (!assign_tmpaction(action, owned_adt)) {
return ActionSlotAssignmentResult::MissingAction;
}
return generic_assign_action_slot_handle(slot_handle,
owned_adt.owner_id,
owned_adt.adt.tmpact,
owned_adt.adt.tmp_slot_handle,
owned_adt.adt.tmp_last_slot_identifier);
}
Action *get_action(ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (!adt) {
return nullptr;
}
if (!adt->action) {
return nullptr;
}
return &adt->action->wrap();
}
std::optional<std::pair<Action *, Slot *>> get_action_slot_pair(ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (!adt || !adt->action) {
/* Not animated by any Action. */
return std::nullopt;
}
Action &action = adt->action->wrap();
Slot *slot = action.slot_for_handle(adt->slot_handle);
if (!slot) {
/* Will not receive any animation from this Action. */
return std::nullopt;
}
return std::make_pair(&action, slot);
}
/* ----- ActionStrip implementation ----------- */
Strip &Strip::create(Action &owning_action, const Strip::Type type)
{
/* Create the strip. */
ActionStrip *strip = MEM_callocN<ActionStrip>(__func__);
*strip = *DNA_struct_default_get(ActionStrip);
strip->strip_type = int8_t(type);
/* Create the strip's data on the owning Action. */
switch (type) {
case Strip::Type::Keyframe: {
StripKeyframeData *strip_data = MEM_new<StripKeyframeData>(__func__);
strip->data_index = owning_action.strip_keyframe_data_append(strip_data);
break;
}
}
/* This can happen if someone forgets to add a strip type in the `switch`
* above, or if someone is evil and passes an invalid strip type to this
* function. */
BLI_assert_msg(strip->data_index != -1, "Newly created strip has no data.");
return strip->wrap();
}
bool Strip::is_infinite() const
{
return this->frame_start == -std::numeric_limits<float>::infinity() &&
this->frame_end == std::numeric_limits<float>::infinity();
}
bool Strip::contains_frame(const float frame_time) const
{
return this->frame_start <= frame_time && frame_time <= this->frame_end;
}
bool Strip::is_last_frame(const float frame_time) const
{
/* Maybe this needs a more advanced equality check. Implement that when
* we have an actual example case that breaks. */
return this->frame_end == frame_time;
}
void Strip::resize(const float frame_start, const float frame_end)
{
BLI_assert(frame_start <= frame_end);
BLI_assert_msg(frame_start < std::numeric_limits<float>::infinity(),
"only the end frame can be at positive infinity");
BLI_assert_msg(frame_end > -std::numeric_limits<float>::infinity(),
"only the start frame can be at negative infinity");
this->frame_start = frame_start;
this->frame_end = frame_end;
}
template<>
const StripKeyframeData &Strip::data<StripKeyframeData>(const Action &owning_action) const
{
BLI_assert(this->type() == StripKeyframeData::TYPE);
return *owning_action.strip_keyframe_data()[this->data_index];
}
template<> StripKeyframeData &Strip::data<StripKeyframeData>(Action &owning_action)
{
BLI_assert(this->type() == StripKeyframeData::TYPE);
return *owning_action.strip_keyframe_data()[this->data_index];
}
/* ----- ActionStripKeyframeData implementation ----------- */
StripKeyframeData::StripKeyframeData(const StripKeyframeData &other)
: ActionStripKeyframeData(other)
{
this->channelbag_array = MEM_calloc_arrayN<ActionChannelbag *>(other.channelbag_array_num,
__func__);
Span<const Channelbag *> channelbags_src = other.channelbags();
for (int i : channelbags_src.index_range()) {
this->channelbag_array[i] = MEM_new<animrig::Channelbag>(__func__, *other.channelbag(i));
}
}
StripKeyframeData::~StripKeyframeData()
{
for (Channelbag *channelbag_for_slot : this->channelbags()) {
MEM_delete(channelbag_for_slot);
}
MEM_SAFE_FREE(this->channelbag_array);
this->channelbag_array_num = 0;
}
blender::Span<const Channelbag *> StripKeyframeData::channelbags() const
{
return blender::Span<Channelbag *>{reinterpret_cast<Channelbag **>(this->channelbag_array),
this->channelbag_array_num};
}
blender::Span<Channelbag *> StripKeyframeData::channelbags()
{
return blender::Span<Channelbag *>{reinterpret_cast<Channelbag **>(this->channelbag_array),
this->channelbag_array_num};
}
const Channelbag *StripKeyframeData::channelbag(const int64_t index) const
{
return &this->channelbag_array[index]->wrap();
}
Channelbag *StripKeyframeData::channelbag(const int64_t index)
{
return &this->channelbag_array[index]->wrap();
}
const Channelbag *StripKeyframeData::channelbag_for_slot(const slot_handle_t slot_handle) const
{
for (const Channelbag *channels : this->channelbags()) {
if (channels->slot_handle == slot_handle) {
return channels;
}
}
return nullptr;
}
int64_t StripKeyframeData::find_channelbag_index(const Channelbag &channelbag) const
{
for (int64_t index = 0; index < this->channelbag_array_num; index++) {
if (this->channelbag(index) == &channelbag) {
return index;
}
}
return -1;
}
Channelbag *StripKeyframeData::channelbag_for_slot(const slot_handle_t slot_handle)
{
const auto *const_this = const_cast<const StripKeyframeData *>(this);
const auto *const_channels = const_this->channelbag_for_slot(slot_handle);
return const_cast<Channelbag *>(const_channels);
}
const Channelbag *StripKeyframeData::channelbag_for_slot(const Slot &slot) const
{
return this->channelbag_for_slot(slot.handle);
}
Channelbag *StripKeyframeData::channelbag_for_slot(const Slot &slot)
{
return this->channelbag_for_slot(slot.handle);
}
Channelbag &StripKeyframeData::channelbag_for_slot_add(const Slot &slot)
{
return this->channelbag_for_slot_add(slot.handle);
}
Channelbag &StripKeyframeData::channelbag_for_slot_add(const slot_handle_t slot_handle)
{
BLI_assert_msg(channelbag_for_slot(slot_handle) == nullptr,
"Cannot add channelbag for already-registered slot");
BLI_assert_msg(slot_handle != Slot::unassigned, "Cannot add channelbag for 'unassigned' slot");
Channelbag &channels = MEM_new<ActionChannelbag>(__func__)->wrap();
channels.slot_handle = slot_handle;
grow_array_and_append<ActionChannelbag *>(
&this->channelbag_array, &this->channelbag_array_num, &channels);
return channels;
}
Channelbag &StripKeyframeData::channelbag_for_slot_ensure(const Slot &slot)
{
return this->channelbag_for_slot_ensure(slot.handle);
}
Channelbag &StripKeyframeData::channelbag_for_slot_ensure(const slot_handle_t slot_handle)
{
Channelbag *channelbag = this->channelbag_for_slot(slot_handle);
if (channelbag != nullptr) {
return *channelbag;
}
return this->channelbag_for_slot_add(slot_handle);
}
static void channelbag_ptr_destructor(ActionChannelbag **dna_channelbag_ptr)
{
Channelbag &channelbag = (*dna_channelbag_ptr)->wrap();
MEM_delete(&channelbag);
};
bool StripKeyframeData::channelbag_remove(Channelbag &channelbag_to_remove)
{
const int64_t channelbag_index = this->find_channelbag_index(channelbag_to_remove);
if (channelbag_index < 0) {
return false;
}
dna::array::remove_index(&this->channelbag_array,
&this->channelbag_array_num,
nullptr,
channelbag_index,
channelbag_ptr_destructor);
return true;
}
void StripKeyframeData::slot_data_remove(const slot_handle_t slot_handle)
{
Channelbag *channelbag = this->channelbag_for_slot(slot_handle);
if (!channelbag) {
return;
}
this->channelbag_remove(*channelbag);
}
void StripKeyframeData::slot_data_duplicate(const slot_handle_t source_slot_handle,
const slot_handle_t target_slot_handle)
{
BLI_assert(!this->channelbag_for_slot(target_slot_handle));
const Channelbag *source_cbag = this->channelbag_for_slot(source_slot_handle);
if (!source_cbag) {
return;
}
Channelbag &target_cbag = *MEM_new<animrig::Channelbag>(__func__, *source_cbag);
target_cbag.slot_handle = target_slot_handle;
grow_array_and_append<ActionChannelbag *>(
&this->channelbag_array, &this->channelbag_array_num, &target_cbag);
}
const FCurve *Channelbag::fcurve_find(const FCurveDescriptor &fcurve_descriptor) const
{
return animrig::fcurve_find(this->fcurves(), fcurve_descriptor);
}
FCurve *Channelbag::fcurve_find(const FCurveDescriptor &fcurve_descriptor)
{
/* Intermediate variable needed to disambiguate const/non-const overloads. */
Span<FCurve *> fcurves = this->fcurves();
return animrig::fcurve_find(fcurves, fcurve_descriptor);
}
FCurve &Channelbag::fcurve_ensure(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
if (FCurve *existing_fcurve = this->fcurve_find(fcurve_descriptor)) {
return *existing_fcurve;
}
return this->fcurve_create(bmain, fcurve_descriptor);
}
FCurve *Channelbag::fcurve_create_unique(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
if (this->fcurve_find(fcurve_descriptor)) {
return nullptr;
}
return &this->fcurve_create(bmain, fcurve_descriptor);
}
FCurve &Channelbag::fcurve_create(Main *bmain, const FCurveDescriptor &fcurve_descriptor)
{
FCurve *new_fcurve = create_fcurve_for_channel(fcurve_descriptor);
if (this->fcurve_array_num == 0) {
new_fcurve->flag |= FCURVE_ACTIVE; /* First curve is added active. */
}
bActionGroup *group = fcurve_descriptor.channel_group.has_value() ?
&this->channel_group_ensure(*fcurve_descriptor.channel_group) :
nullptr;
const int insert_index = group ? group->fcurve_range_start + group->fcurve_range_length :
this->fcurve_array_num;
BLI_assert(insert_index <= this->fcurve_array_num);
grow_array_and_insert(&this->fcurve_array, &this->fcurve_array_num, insert_index, new_fcurve);
if (group) {
group->fcurve_range_length += 1;
this->restore_channel_group_invariants();
}
if (bmain) {
DEG_relations_tag_update(bmain);
}
return *new_fcurve;
}
Vector<FCurve *> Channelbag::fcurve_create_many(Main *bmain,
Span<FCurveDescriptor> fcurve_descriptors)
{
const int prev_fcurve_num = this->fcurve_array_num;
const int add_fcurve_num = int(fcurve_descriptors.size());
const bool make_first_active = prev_fcurve_num == 0;
/* Figure out which path+index combinations already exist. */
struct CurvePathIndex {
StringRefNull rna_path;
int array_index;
bool operator==(const CurvePathIndex &o) const
{
/* Check indices first, cheaper than a string comparison. */
return this->array_index == o.array_index && this->rna_path == o.rna_path;
}
uint64_t hash() const
{
return get_default_hash(this->rna_path, this->array_index);
}
};
Set<CurvePathIndex> unique_curves;
unique_curves.reserve(prev_fcurve_num);
for (FCurve *fcurve : this->fcurves()) {
CurvePathIndex path_index;
path_index.rna_path = StringRefNull(fcurve->rna_path ? fcurve->rna_path : "");
path_index.array_index = fcurve->array_index;
unique_curves.add(path_index);
}
/* Grow curves array with enough space for new curves. */
grow_array(&this->fcurve_array, &this->fcurve_array_num, add_fcurve_num);
/* Add the new curves. */
Vector<FCurve *> new_fcurves;
new_fcurves.resize(add_fcurve_num);
int curve_index = prev_fcurve_num;
for (int i = 0; i < add_fcurve_num; i++) {
const FCurveDescriptor &desc = fcurve_descriptors[i];
CurvePathIndex path_index;
path_index.rna_path = desc.rna_path;
path_index.array_index = desc.array_index;
if (desc.rna_path.is_empty() || !unique_curves.add(path_index)) {
/* Empty input path, or such curve already exists. */
new_fcurves[i] = nullptr;
continue;
}
FCurve *fcurve = create_fcurve_for_channel(desc);
new_fcurves[i] = fcurve;
this->fcurve_array[curve_index] = fcurve;
if (desc.channel_group.has_value()) {
bActionGroup *group = &this->channel_group_ensure(*desc.channel_group);
const int insert_index = group->fcurve_range_start + group->fcurve_range_length;
BLI_assert(insert_index <= this->fcurve_array_num);
/* Insert curve into proper array place at the end of the group. Note: this can
* still lead to quadratic complexity, in practice was not found to be an issue yet. */
array_shift_range(
this->fcurve_array, this->fcurve_array_num, curve_index, curve_index + 1, insert_index);
group->fcurve_range_length++;
/* Update curve start ranges of the following groups. */
int index = this->channel_group_find_index(group);
BLI_assert(index >= 0 && index < this->group_array_num);
for (index = index + 1; index < this->group_array_num; index++) {
this->group_array[index]->fcurve_range_start++;
}
}
curve_index++;
}
if (this->fcurve_array_num != curve_index) {
/* Some curves were not created, resize to final amount. */
shrink_array(
&this->fcurve_array, &this->fcurve_array_num, this->fcurve_array_num - curve_index);
}
if (make_first_active) {
/* Set first created curve as active. */
for (FCurve *fcurve : new_fcurves) {
if (fcurve != nullptr) {
fcurve->flag |= FCURVE_ACTIVE;
break;
}
}
}
this->restore_channel_group_invariants();
if (bmain) {
DEG_relations_tag_update(bmain);
}
return new_fcurves;
}
void Channelbag::fcurve_append(FCurve &fcurve)
{
/* Appended F-Curves don't belong to any group yet, so better make sure their
* group pointer reflects that. */
fcurve.grp = nullptr;
grow_array_and_append(&this->fcurve_array, &this->fcurve_array_num, &fcurve);
}
static void fcurve_ptr_destructor(FCurve **fcurve_ptr)
{
BKE_fcurve_free(*fcurve_ptr);
};
bool Channelbag::fcurve_remove(FCurve &fcurve_to_remove)
{
if (!this->fcurve_detach(fcurve_to_remove)) {
return false;
}
BKE_fcurve_free(&fcurve_to_remove);
return true;
}
void Channelbag::fcurve_remove_by_index(const int64_t fcurve_index)
{
/* Grab the pointer before it's detached, so we can free it after. */
FCurve *fcurve_to_remove = this->fcurve(fcurve_index);
this->fcurve_detach_by_index(fcurve_index);
BKE_fcurve_free(fcurve_to_remove);
}
static void fcurve_ptr_noop_destructor(FCurve ** /*fcurve_ptr*/) {}
bool Channelbag::fcurve_detach(FCurve &fcurve_to_detach)
{
const int64_t fcurve_index = this->fcurves().first_index_try(&fcurve_to_detach);
if (fcurve_index < 0) {
return false;
}
this->fcurve_detach_by_index(fcurve_index);
return true;
}
void Channelbag::fcurve_detach_by_index(const int64_t fcurve_index)
{
BLI_assert(fcurve_index >= 0);
BLI_assert(fcurve_index < this->fcurve_array_num);
const int group_index = this->channel_group_containing_index(fcurve_index);
if (group_index != -1) {
bActionGroup *group = this->channel_group(group_index);
group->fcurve_range_length -= 1;
if (group->fcurve_range_length <= 0) {
const int group_index = this->channel_groups().first_index_try(group);
this->channel_group_remove_raw(group_index);
}
}
dna::array::remove_index(&this->fcurve_array,
&this->fcurve_array_num,
nullptr,
fcurve_index,
fcurve_ptr_noop_destructor);
this->restore_channel_group_invariants();
/* As an optimization, this function could call `DEG_relations_tag_update(bmain)` to prune any
* relationships that are now no longer necessary. This is not needed for correctness of the
* depsgraph evaluation results though. */
}
void Channelbag::fcurve_move_to_index(FCurve &fcurve, int to_fcurve_index)
{
BLI_assert(to_fcurve_index >= 0 && to_fcurve_index < this->fcurves().size());
const int fcurve_index = this->fcurves().first_index_try(&fcurve);
BLI_assert_msg(fcurve_index >= 0, "FCurve not in this channel bag.");
array_shift_range(
this->fcurve_array, this->fcurve_array_num, fcurve_index, fcurve_index + 1, to_fcurve_index);
this->restore_channel_group_invariants();
}
void Channelbag::fcurves_clear()
{
dna::array::clear(&this->fcurve_array, &this->fcurve_array_num, nullptr, fcurve_ptr_destructor);
/* Since all F-Curves are gone, the groups are all empty. */
for (bActionGroup *group : channel_groups()) {
group->fcurve_range_start = 0;
group->fcurve_range_length = 0;
}
}
static void cyclic_keying_ensure_modifier(FCurve &fcurve)
{
/* #BKE_fcurve_get_cycle_type() only looks at the first modifier to see if it's a Cycle modifier,
* so if we're going to add one, better make sure it's the first one.
*
* BUT: #add_fmodifier() only allows adding a Cycle modifier when there are none yet, so that's
* all that we need to check for here.
*/
if (!BLI_listbase_is_empty(&fcurve.modifiers)) {
return;
}
add_fmodifier(&fcurve.modifiers, FMODIFIER_TYPE_CYCLES, &fcurve);
}
/**
* Ensure there are at least two keys in this F-Curve, in order to define A cycle range.
*
* That range does NOT have to be the same as `cycle_range` -- that parameter is only used to
* insert a 2nd key when there is only one.
*
* \note This function ONLY does something when there is a single keyframe. If there are more, the
* first and last keys already define the cycle range. If that range is not the same as the
* `cycle_range` parameter, it's seen as an animator's choice and won't be adjusted.
*/
static void cyclic_keying_ensure_cycle_range_exists(FCurve &fcurve, const float2 cycle_range)
{
/* This is basically a copy of the legacy function `make_new_fcurve_cyclic()`
* in `keyframing.cc`, except that it's limited to only one thing (ensuring
* two keys exist to make cycling possible). Creating the F-Curve modifier is
* the responsibility of another function. */
if (fcurve.totvert != 1 || fcurve.bezt == nullptr) {
return;
}
const float period = cycle_range[1] - cycle_range[0];
if (period < 0.1f) {
return;
}
/* Move the one existing keyframe into the cycle range. */
const float frame_offset = fcurve.bezt[0].vec[1][0] - cycle_range[0];
const float fix = floorf(frame_offset / period) * period;
fcurve.bezt[0].vec[0][0] -= fix;
fcurve.bezt[0].vec[1][0] -= fix;
fcurve.bezt[0].vec[2][0] -= fix;
/* Reallocate the array to make space for the 2nd point. */
fcurve.totvert++;
fcurve.bezt = static_cast<BezTriple *>(
MEM_reallocN(fcurve.bezt, sizeof(BezTriple) * fcurve.totvert));
/* Duplicate and offset the keyframe. */
fcurve.bezt[1] = fcurve.bezt[0];
fcurve.bezt[1].vec[0][0] += period;
fcurve.bezt[1].vec[1][0] += period;
fcurve.bezt[1].vec[2][0] += period;
}
SingleKeyingResult StripKeyframeData::keyframe_insert(Main *bmain,
const Slot &slot,
const FCurveDescriptor &fcurve_descriptor,
const float2 time_value,
const KeyframeSettings &settings,
const eInsertKeyFlags insert_key_flags,
const std::optional<float2> cycle_range)
{
/* Get the fcurve, or create one if it doesn't exist and the keying flags
* allow. */
FCurve *fcurve = nullptr;
if (key_insertion_may_create_fcurve(insert_key_flags)) {
fcurve = &this->channelbag_for_slot_ensure(slot).fcurve_ensure(bmain, fcurve_descriptor);
}
else {
Channelbag *channels = this->channelbag_for_slot(slot);
if (channels != nullptr) {
fcurve = channels->fcurve_find(fcurve_descriptor);
}
}
if (!fcurve) {
CLOG_WARN(&LOG,
"FCurve %s[%d] for slot %s was not created due to either the Only Insert "
"Available setting or Replace keyframing mode.\n",
fcurve_descriptor.rna_path.c_str(),
fcurve_descriptor.array_index,
slot.identifier);
return SingleKeyingResult::CANNOT_CREATE_FCURVE;
}
if (!BKE_fcurve_is_keyframable(fcurve)) {
/* TODO: handle this properly, in a way that can be communicated to the user. */
CLOG_WARN(&LOG,
"FCurve %s[%d] for slot %s doesn't allow inserting keys.\n",
fcurve_descriptor.rna_path.c_str(),
fcurve_descriptor.array_index,
slot.identifier);
return SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE;
}
if (cycle_range && (*cycle_range)[0] < (*cycle_range)[1]) {
/* Cyclic keying consists of three things:
* - Ensure there is a Cycle modifier on the F-Curve.
* - Ensure the start and end of the cycle have explicit keys, so that the
* cycle modifier knows how to cycle (it doesn't look at the Action, and
* as long as the period is correct, the first/last keys don't have to
* align with the Action start/end).
* - Offset the key to insert so that it falls within the cycle range.
*/
cyclic_keying_ensure_modifier(*fcurve);
cyclic_keying_ensure_cycle_range_exists(*fcurve, *cycle_range);
/* Offsetting the key doesn't have to happen here, as insert_vert_fcurve()
* takes care of that. */
}
const SingleKeyingResult insert_vert_result = insert_vert_fcurve(
fcurve, time_value, settings, insert_key_flags);
if (insert_vert_result != SingleKeyingResult::SUCCESS) {
CLOG_WARN(&LOG,
"Could not insert key into FCurve %s[%d] for slot %s.\n",
fcurve_descriptor.rna_path.c_str(),
fcurve_descriptor.array_index,
slot.identifier);
return insert_vert_result;
}
if (fcurve_descriptor.prop_type) {
update_autoflags_fcurve_direct(fcurve, *fcurve_descriptor.prop_type);
}
return SingleKeyingResult::SUCCESS;
}
/* ActionChannelbag implementation. */
Channelbag::Channelbag(const Channelbag &other)
{
this->slot_handle = other.slot_handle;
this->fcurve_array_num = other.fcurve_array_num;
this->fcurve_array = MEM_calloc_arrayN<FCurve *>(other.fcurve_array_num, __func__);
for (int i = 0; i < other.fcurve_array_num; i++) {
const FCurve *fcu_src = other.fcurve_array[i];
this->fcurve_array[i] = BKE_fcurve_copy(fcu_src);
}
this->group_array_num = other.group_array_num;
this->group_array = MEM_calloc_arrayN<bActionGroup *>(other.group_array_num, __func__);
for (int i = 0; i < other.group_array_num; i++) {
const bActionGroup *group_src = other.group_array[i];
this->group_array[i] = static_cast<bActionGroup *>(MEM_dupallocN(group_src));
this->group_array[i]->channelbag = this;
}
/* BKE_fcurve_copy() resets the FCurve's group pointer. Which is good, because the groups are
* duplicated too. This sets the group pointers to the correct values. */
this->restore_channel_group_invariants();
}
Channelbag::~Channelbag()
{
for (FCurve *fcu : this->fcurves()) {
BKE_fcurve_free(fcu);
}
MEM_SAFE_FREE(this->fcurve_array);
this->fcurve_array_num = 0;
for (bActionGroup *group : this->channel_groups()) {
MEM_SAFE_FREE(group);
}
MEM_SAFE_FREE(this->group_array);
this->group_array_num = 0;
}
blender::Span<const FCurve *> Channelbag::fcurves() const
{
return blender::Span<FCurve *>{this->fcurve_array, this->fcurve_array_num};
}
blender::Span<FCurve *> Channelbag::fcurves()
{
return blender::Span<FCurve *>{this->fcurve_array, this->fcurve_array_num};
}
const FCurve *Channelbag::fcurve(const int64_t index) const
{
return this->fcurve_array[index];
}
FCurve *Channelbag::fcurve(const int64_t index)
{
return this->fcurve_array[index];
}
blender::Span<const bActionGroup *> Channelbag::channel_groups() const
{
return blender::Span<bActionGroup *>{this->group_array, this->group_array_num};
}
blender::Span<bActionGroup *> Channelbag::channel_groups()
{
return blender::Span<bActionGroup *>{this->group_array, this->group_array_num};
}
const bActionGroup *Channelbag::channel_group(const int64_t index) const
{
BLI_assert(index < this->group_array_num);
return this->group_array[index];
}
bActionGroup *Channelbag::channel_group(const int64_t index)
{
BLI_assert(index < this->group_array_num);
return this->group_array[index];
}
const bActionGroup *Channelbag::channel_group_find(const StringRef name) const
{
for (const bActionGroup *group : this->channel_groups()) {
if (name == StringRef{group->name}) {
return group;
}
}
return nullptr;
}
int Channelbag::channel_group_find_index(const bActionGroup *group) const
{
for (int i = 0; i < this->group_array_num; i++) {
if (this->group_array[i] == group) {
return i;
}
}
return -1;
}
bActionGroup *Channelbag::channel_group_find(const StringRef name)
{
/* Intermediate variable needed to disambiguate const/non-const overloads. */
Span<bActionGroup *> groups = this->channel_groups();
for (bActionGroup *group : groups) {
if (name == StringRef{group->name}) {
return group;
}
}
return nullptr;
}
int Channelbag::channel_group_containing_index(const int fcurve_array_index)
{
int i = 0;
for (const bActionGroup *group : this->channel_groups()) {
if (fcurve_array_index >= group->fcurve_range_start &&
fcurve_array_index < (group->fcurve_range_start + group->fcurve_range_length))
{
return i;
}
i++;
}
return -1;
}
bActionGroup &Channelbag::channel_group_create(StringRefNull name)
{
bActionGroup *new_group = MEM_callocN<bActionGroup>(__func__);
/* Find the end fcurve index of the current channel groups, to be used as the
* start of the new channel group. */
int fcurve_index = 0;
const int length = this->channel_groups().size();
if (length > 0) {
const bActionGroup *last = this->channel_group(length - 1);
fcurve_index = last->fcurve_range_start + last->fcurve_range_length;
}
new_group->fcurve_range_start = fcurve_index;
new_group->channelbag = this;
/* Make it selected. */
new_group->flag = AGRP_SELECTED;
/* Ensure it has a unique name.
*
* Note that this only happens here (upon creation). The user can later rename
* groups to have duplicate names. This is stupid, but it's how the legacy
* system worked, and at the time of writing this code we're just trying to
* match that system's behavior, even when it's goofy. */
std::string unique_name = BLI_uniquename_cb(
[&](const StringRef name) {
for (const bActionGroup *group : this->channel_groups()) {
if (STREQ(group->name, name.data())) {
return true;
}
}
return false;
},
'.',
name[0] == '\0' ? DATA_("Group") : name);
STRNCPY_UTF8(new_group->name, unique_name.c_str());
grow_array_and_append(&this->group_array, &this->group_array_num, new_group);
return *new_group;
}
bActionGroup &Channelbag::channel_group_ensure(StringRefNull name)
{
bActionGroup *group = this->channel_group_find(name);
if (group) {
return *group;
}
return this->channel_group_create(name);
}
bool Channelbag::channel_group_remove(bActionGroup &group)
{
const int group_index = this->channel_groups().first_index_try(&group);
if (group_index == -1) {
return false;
}
/* Move the group's fcurves to just past the end of where the grouped
* fcurves will be after this group is removed. */
const bActionGroup *last_group = this->channel_groups().last();
BLI_assert(last_group != nullptr);
const int to_index = last_group->fcurve_range_start + last_group->fcurve_range_length -
group.fcurve_range_length;
array_shift_range(this->fcurve_array,
this->fcurve_array_num,
group.fcurve_range_start,
group.fcurve_range_start + group.fcurve_range_length,
to_index);
this->channel_group_remove_raw(group_index);
this->restore_channel_group_invariants();
return true;
}
void Channelbag::channel_group_move_to_index(bActionGroup &group, const int to_group_index)
{
BLI_assert(to_group_index >= 0 && to_group_index < this->channel_groups().size());
const int group_index = this->channel_groups().first_index_try(&group);
BLI_assert_msg(group_index >= 0, "Group not in this channel bag.");
/* Shallow copy, to track which fcurves should be moved in the second step. */
const bActionGroup pre_move_group = group;
/* First we move the group to its new position. The call to
* `restore_channel_group_invariants()` is necessary to update the group's
* fcurve range (as well as the ranges of the other groups) to match its new
* position in the group array. */
array_shift_range(
this->group_array, this->group_array_num, group_index, group_index + 1, to_group_index);
this->restore_channel_group_invariants();
/* Move the fcurves that were part of `group` (as recorded in
*`pre_move_group`) to their new positions (now in `group`) so that they're
* part of `group` again. */
array_shift_range(this->fcurve_array,
this->fcurve_array_num,
pre_move_group.fcurve_range_start,
pre_move_group.fcurve_range_start + pre_move_group.fcurve_range_length,
group.fcurve_range_start);
this->restore_channel_group_invariants();
}
void Channelbag::channel_group_remove_raw(const int group_index)
{
BLI_assert(group_index >= 0 && group_index < this->channel_groups().size());
MEM_SAFE_FREE(this->group_array[group_index]);
shrink_array_and_remove(&this->group_array, &this->group_array_num, group_index);
}
void Channelbag::restore_channel_group_invariants()
{
/* Shift channel groups. */
{
int start_index = 0;
for (bActionGroup *group : this->channel_groups()) {
group->fcurve_range_start = start_index;
start_index += group->fcurve_range_length;
}
/* Double-check that this didn't push any of the groups off the end of the
* fcurve array. */
BLI_assert(start_index <= this->fcurve_array_num);
}
/* Recompute fcurves' group pointers. */
{
for (FCurve *fcurve : this->fcurves()) {
fcurve->grp = nullptr;
}
for (bActionGroup *group : this->channel_groups()) {
for (FCurve *fcurve : group->wrap().fcurves()) {
fcurve->grp = group;
}
}
}
}
bool ChannelGroup::is_legacy() const
{
return this->channelbag == nullptr;
}
Span<FCurve *> ChannelGroup::fcurves()
{
BLI_assert(!this->is_legacy());
if (this->fcurve_range_length == 0) {
return {};
}
return this->channelbag->wrap().fcurves().slice(this->fcurve_range_start,
this->fcurve_range_length);
}
Span<const FCurve *> ChannelGroup::fcurves() const
{
BLI_assert(!this->is_legacy());
if (this->fcurve_range_length == 0) {
return {};
}
return this->channelbag->wrap().fcurves().slice(this->fcurve_range_start,
this->fcurve_range_length);
}
/* Utility function implementations. */
const animrig::Channelbag *channelbag_for_action_slot(const Action &action,
const slot_handle_t slot_handle)
{
assert_baklava_phase_1_invariants(action);
if (slot_handle == Slot::unassigned) {
return nullptr;
}
for (const animrig::Layer *layer : action.layers()) {
for (const animrig::Strip *strip : layer->strips()) {
switch (strip->type()) {
case animrig::Strip::Type::Keyframe: {
const animrig::StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(
action);
const animrig::Channelbag *bag = strip_data.channelbag_for_slot(slot_handle);
if (bag) {
return bag;
}
}
}
}
}
return nullptr;
}
animrig::Channelbag *channelbag_for_action_slot(Action &action, const slot_handle_t slot_handle)
{
const animrig::Channelbag *const_bag = channelbag_for_action_slot(
const_cast<const Action &>(action), slot_handle);
return const_cast<animrig::Channelbag *>(const_bag);
}
Span<FCurve *> fcurves_for_action_slot(Action &action, const slot_handle_t slot_handle)
{
BLI_assert(action.is_action_layered());
assert_baklava_phase_1_invariants(action);
animrig::Channelbag *bag = channelbag_for_action_slot(action, slot_handle);
if (!bag) {
return {};
}
return bag->fcurves();
}
Span<const FCurve *> fcurves_for_action_slot(const Action &action, const slot_handle_t slot_handle)
{
BLI_assert(action.is_action_layered());
assert_baklava_phase_1_invariants(action);
const animrig::Channelbag *bag = channelbag_for_action_slot(action, slot_handle);
if (!bag) {
return {};
}
return bag->fcurves();
}
FCurve *fcurve_find_in_action(bAction *act, const FCurveDescriptor &fcurve_descriptor)
{
if (act == nullptr) {
return nullptr;
}
Action &action = act->wrap();
if (action.is_action_legacy()) {
return BKE_fcurve_find(
&act->curves, fcurve_descriptor.rna_path.c_str(), fcurve_descriptor.array_index);
}
assert_baklava_phase_1_invariants(action);
Layer *layer = action.layer(0);
if (!layer) {
return nullptr;
}
Strip *strip = layer->strip(0);
if (!strip) {
return nullptr;
}
StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);
for (Channelbag *channelbag : strip_data.channelbags()) {
FCurve *fcu = channelbag->fcurve_find(fcurve_descriptor);
if (fcu) {
return fcu;
}
}
return nullptr;
}
FCurve *fcurve_find_in_assigned_slot(AnimData &adt, const FCurveDescriptor &fcurve_descriptor)
{
return fcurve_find_in_action_slot(adt.action, adt.slot_handle, fcurve_descriptor);
}
FCurve *fcurve_find_in_action_slot(bAction *act,
const slot_handle_t slot_handle,
const FCurveDescriptor &fcurve_descriptor)
{
if (act == nullptr) {
return nullptr;
}
Action &action = act->wrap();
if (action.is_action_legacy()) {
return BKE_fcurve_find(
&act->curves, fcurve_descriptor.rna_path.c_str(), fcurve_descriptor.array_index);
}
Channelbag *cbag = channelbag_for_action_slot(action, slot_handle);
if (!cbag) {
return nullptr;
}
return cbag->fcurve_find(fcurve_descriptor);
}
bool fcurve_matches_collection_path(const FCurve &fcurve,
const StringRefNull collection_rna_path,
const StringRefNull data_name)
{
BLI_assert(!collection_rna_path.is_empty());
const size_t quoted_name_size = data_name.size() + 1;
char *quoted_name = static_cast<char *>(alloca(quoted_name_size));
if (!fcurve.rna_path) {
return false;
}
/* Skipping names longer than `quoted_name_size` is OK since we're after an exact match. */
if (!BLI_str_quoted_substr(
fcurve.rna_path, collection_rna_path.c_str(), quoted_name, quoted_name_size))
{
return false;
}
if (quoted_name != data_name) {
return false;
}
return true;
}
Vector<FCurve *> fcurves_in_action_slot_filtered(bAction *act,
const slot_handle_t slot_handle,
FunctionRef<bool(const FCurve &fcurve)> predicate)
{
BLI_assert(act);
Vector<FCurve *> found;
foreach_fcurve_in_action_slot(act->wrap(), slot_handle, [&](FCurve &fcurve) {
if (predicate(fcurve)) {
found.append(&fcurve);
}
});
return found;
}
Vector<FCurve *> fcurves_in_span_filtered(Span<FCurve *> fcurves,
FunctionRef<bool(const FCurve &fcurve)> predicate)
{
Vector<FCurve *> found;
for (FCurve *fcurve : fcurves) {
if (predicate(*fcurve)) {
found.append(fcurve);
}
}
return found;
}
Vector<FCurve *> fcurves_in_listbase_filtered(ListBase /* FCurve * */ fcurves,
FunctionRef<bool(const FCurve &fcurve)> predicate)
{
Vector<FCurve *> found;
LISTBASE_FOREACH (FCurve *, fcurve, &fcurves) {
if (predicate(*fcurve)) {
found.append(fcurve);
}
}
return found;
}
FCurve *action_fcurve_ensure_ex(Main *bmain,
bAction *act,
const char group[],
PointerRNA *ptr,
const FCurveDescriptor &fcurve_descriptor)
{
if (act == nullptr) {
return nullptr;
}
if (animrig::legacy::action_treat_as_legacy(*act)) {
return action_fcurve_ensure_legacy(bmain, act, group, ptr, fcurve_descriptor);
}
/* NOTE: for layered actions we require the following:
*
* - `ptr` is non-null.
* - `ptr` has an `owner_id` that already uses `act`.
*
* This isn't for any principled reason, but rather is because adding
* support for layered actions to this function was a fix to make Follow
* Path animation work properly with layered actions (see PR #124353), and
* those are the requirements the Follow Path code conveniently met.
* Moreover those requirements were also already met by the other call sites
* that potentially call this function with layered actions.
*
* Trying to puzzle out what "should" happen when these requirements don't
* hold, or if this is even the best place to handle the layered action
* cases at all, was leading to discussion of larger changes than made sense
* to tackle at that point. */
BLI_assert(ptr != nullptr);
if (ptr == nullptr || ptr->owner_id == nullptr) {
return nullptr;
}
return &action_fcurve_ensure(bmain, *act, *ptr->owner_id, fcurve_descriptor);
}
Channelbag &action_channelbag_ensure(bAction &dna_action, ID &animated_id)
{
Action &action = dna_action.wrap();
BLI_assert(get_action(animated_id) == &action);
/* Ensure the id has an assigned slot. */
Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
/* A nullptr here means the ID type is not animatable. But since the Action is already assigned,
* it is certain that the ID is actually animatable. */
BLI_assert(slot);
action.layer_keystrip_ensure();
assert_baklava_phase_1_invariants(action);
StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
return strip_data.channelbag_for_slot_ensure(*slot);
}
FCurve &action_fcurve_ensure(Main *bmain,
bAction &dna_action,
ID &animated_id,
const FCurveDescriptor &fcurve_descriptor)
{
Channelbag &channelbag = action_channelbag_ensure(dna_action, animated_id);
return channelbag.fcurve_ensure(bmain, fcurve_descriptor);
}
FCurve *action_fcurve_ensure_legacy(Main *bmain,
bAction *act,
const char group[],
PointerRNA *ptr,
const FCurveDescriptor &fcurve_descriptor)
{
if (!act) {
return nullptr;
}
BLI_assert(act->wrap().is_empty() || act->wrap().is_action_legacy());
/* Try to find f-curve matching for this setting.
* - add if not found and allowed to add one
* TODO: add auto-grouping support? how this works will need to be resolved
*/
FCurve *fcu = animrig::fcurve_find_in_action(act, fcurve_descriptor);
if (fcu != nullptr) {
return fcu;
}
/* Determine the property (sub)type if we can. */
std::optional<PropertyType> prop_type = std::nullopt;
std::optional<PropertySubType> prop_subtype = std::nullopt;
if (ptr != nullptr) {
PropertyRNA *resolved_prop;
PointerRNA resolved_ptr;
PointerRNA id_ptr = RNA_id_pointer_create(ptr->owner_id);
const bool resolved = RNA_path_resolve_property(
&id_ptr, fcurve_descriptor.rna_path.c_str(), &resolved_ptr, &resolved_prop);
if (resolved) {
prop_type = RNA_property_type(resolved_prop);
prop_subtype = RNA_property_subtype(resolved_prop);
}
}
BLI_assert_msg(!fcurve_descriptor.prop_type.has_value(),
"Did not expect a prop_type to be passed in. This is fine, but does need some "
"changes to action_fcurve_ensure_legacy() to deal with it");
BLI_assert_msg(!fcurve_descriptor.prop_subtype.has_value(),
"Did not expect a prop_subtype to be passed in. This is fine, but does need some "
"changes to action_fcurve_ensure_legacy() to deal with it");
fcu = create_fcurve_for_channel(
{fcurve_descriptor.rna_path, fcurve_descriptor.array_index, prop_type, prop_subtype});
if (BLI_listbase_is_empty(&act->curves)) {
fcu->flag |= FCURVE_ACTIVE;
}
if (group) {
bActionGroup *agrp = BKE_action_group_find_name(act, group);
if (agrp == nullptr) {
agrp = action_groups_add_new(act, group);
/* Sync bone group colors if applicable. */
if (ptr && (ptr->type == &RNA_PoseBone) && ptr->data) {
const bPoseChannel *pchan = static_cast<const bPoseChannel *>(ptr->data);
action_group_colors_set_from_posebone(agrp, pchan);
}
}
action_groups_add_channel(act, agrp, fcu);
}
else {
BLI_addtail(&act->curves, fcu);
}
/* New f-curve was added, meaning it's possible that it affects
* dependency graph component which wasn't previously animated.
*/
DEG_relations_tag_update(bmain);
return fcu;
}
bool action_fcurve_remove(Action &action, FCurve &fcu)
{
if (action_fcurve_detach(action, fcu)) {
BKE_fcurve_free(&fcu);
return true;
}
return false;
}
bool action_fcurve_detach(Action &action, FCurve &fcurve_to_detach)
{
if (action.is_action_legacy()) {
return BLI_remlink_safe(&action.curves, &fcurve_to_detach);
}
for (Layer *layer : action.layers()) {
for (Strip *strip : layer->strips()) {
if (!(strip->type() == Strip::Type::Keyframe)) {
continue;
}
StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);
for (Channelbag *bag : strip_data.channelbags()) {
const bool is_detached = bag->fcurve_detach(fcurve_to_detach);
if (is_detached) {
return true;
}
}
}
}
return false;
}
void action_fcurve_attach(Action &action,
const slot_handle_t action_slot,
FCurve &fcurve_to_attach,
std::optional<StringRefNull> group_name)
{
if (animrig::legacy::action_treat_as_legacy(action)) {
BLI_addtail(&action.curves, &fcurve_to_attach);
return;
}
Slot *slot = action.slot_for_handle(action_slot);
BLI_assert(slot);
if (!slot) {
printf("Cannot find slot handle %d on Action %s, unable to attach F-Curve %s[%d] to it!\n",
action_slot,
action.id.name + 2,
fcurve_to_attach.rna_path,
fcurve_to_attach.array_index);
return;
}
action.layer_keystrip_ensure();
StripKeyframeData &strip_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
Channelbag &cbag = strip_data.channelbag_for_slot_ensure(*slot);
cbag.fcurve_append(fcurve_to_attach);
if (group_name) {
bActionGroup &group = cbag.channel_group_ensure(*group_name);
cbag.fcurve_assign_to_channel_group(fcurve_to_attach, group);
}
}
void action_fcurve_move(Action &action_dst,
const slot_handle_t action_slot_dst,
Action &action_src,
FCurve &fcurve)
{
/* Store the group name locally, as the group will be removed if this was its
* last F-Curve. */
std::optional<std::string> group_name;
if (fcurve.grp) {
group_name = fcurve.grp->name;
}
const bool is_detached = action_fcurve_detach(action_src, fcurve);
BLI_assert(is_detached);
UNUSED_VARS_NDEBUG(is_detached);
action_fcurve_attach(action_dst, action_slot_dst, fcurve, group_name);
}
void channelbag_fcurves_move(Channelbag &channelbag_dst, Channelbag &channelbag_src)
{
while (!channelbag_src.fcurves().is_empty()) {
FCurve &fcurve = *channelbag_src.fcurve(0);
/* Store the group name locally, as the group will be removed if this was its
* last F-Curve. */
std::optional<std::string> group_name;
if (fcurve.grp) {
group_name = fcurve.grp->name;
}
const bool is_detached = channelbag_src.fcurve_detach(fcurve);
BLI_assert(is_detached);
UNUSED_VARS_NDEBUG(is_detached);
channelbag_dst.fcurve_append(fcurve);
if (group_name) {
bActionGroup &group = channelbag_dst.channel_group_ensure(*group_name);
channelbag_dst.fcurve_assign_to_channel_group(fcurve, group);
}
}
}
bool Channelbag::fcurve_assign_to_channel_group(FCurve &fcurve, bActionGroup &to_group)
{
if (this->channel_groups().first_index_try(&to_group) == -1) {
return false;
}
const int fcurve_index = this->fcurves().first_index_try(&fcurve);
if (fcurve_index == -1) {
return false;
}
if (fcurve.grp == &to_group) {
return true;
}
/* Remove fcurve from old group, if it belongs to one. */
if (fcurve.grp != nullptr) {
fcurve.grp->fcurve_range_length--;
if (fcurve.grp->fcurve_range_length == 0) {
const int group_index = this->channel_groups().first_index_try(fcurve.grp);
this->channel_group_remove_raw(group_index);
}
this->restore_channel_group_invariants();
}
array_shift_range(this->fcurve_array,
this->fcurve_array_num,
fcurve_index,
fcurve_index + 1,
to_group.fcurve_range_start + to_group.fcurve_range_length);
to_group.fcurve_range_length++;
this->restore_channel_group_invariants();
return true;
}
bool Channelbag::fcurve_ungroup(FCurve &fcurve)
{
const int fcurve_index = this->fcurves().first_index_try(&fcurve);
if (fcurve_index == -1) {
return false;
}
if (fcurve.grp == nullptr) {
return true;
}
bActionGroup *old_group = fcurve.grp;
array_shift_range(this->fcurve_array,
this->fcurve_array_num,
fcurve_index,
fcurve_index + 1,
this->fcurve_array_num - 1);
old_group->fcurve_range_length--;
if (old_group->fcurve_range_length == 0) {
const int old_group_index = this->channel_groups().first_index_try(old_group);
this->channel_group_remove_raw(old_group_index);
}
this->restore_channel_group_invariants();
return true;
}
ID *action_slot_get_id_for_keying(Main &bmain,
Action &action,
const slot_handle_t slot_handle,
ID *primary_id)
{
if (animrig::legacy::action_treat_as_legacy(action)) {
if (primary_id && get_action(*primary_id) == &action) {
return primary_id;
}
return nullptr;
}
Slot *slot = action.slot_for_handle(slot_handle);
if (slot == nullptr) {
return nullptr;
}
blender::Span<ID *> users = slot->users(bmain);
if (users.size() == 1) {
/* We only do this for `users.size() == 1` and not `users.size() >= 1`
* because when there's more than one user it's ambiguous which user we
* should return, and that would be unpredictable for end users of Blender.
* We also expect that to be a corner case anyway. So instead we let that
* case either get disambiguated by the primary ID in the case below, or
* return null. */
return users[0];
}
if (users.contains(primary_id)) {
return primary_id;
}
return nullptr;
}
ID *action_slot_get_id_best_guess(Main &bmain, Slot &slot, ID *primary_id)
{
blender::Span<ID *> users = slot.users(bmain);
if (users.is_empty()) {
return nullptr;
}
if (users.contains(primary_id)) {
return primary_id;
}
return users[0];
}
slot_handle_t first_slot_handle(const ::bAction &dna_action)
{
const Action &action = dna_action.wrap();
if (action.slot_array_num == 0) {
return Slot::unassigned;
}
return action.slot_array[0]->handle;
}
void assert_baklava_phase_1_invariants(const Action &action)
{
if (action.is_action_legacy()) {
return;
}
if (action.layers().is_empty()) {
return;
}
BLI_assert(action.layers().size() == 1);
assert_baklava_phase_1_invariants(*action.layer(0));
}
void assert_baklava_phase_1_invariants(const Layer &layer)
{
if (layer.strips().is_empty()) {
return;
}
BLI_assert(layer.strips().size() == 1);
assert_baklava_phase_1_invariants(*layer.strip(0));
}
void assert_baklava_phase_1_invariants(const Strip &strip)
{
UNUSED_VARS_NDEBUG(strip);
BLI_assert(strip.type() == Strip::Type::Keyframe);
BLI_assert(strip.is_infinite());
BLI_assert(strip.frame_offset == 0.0);
}
Action *convert_to_layered_action(Main &bmain, const Action &legacy_action)
{
if (!legacy_action.is_action_legacy()) {
return nullptr;
}
std::string suffix = "_layered";
/* In case the legacy action has a long name it is shortened to make space for the suffix. */
char legacy_name[MAX_ID_NAME - 10];
/* Offsetting the id.name to remove the ID prefix (AC) which gets added back later. */
STRNCPY_UTF8(legacy_name, legacy_action.id.name + 2);
const std::string layered_action_name = std::string(legacy_name) + suffix;
bAction *dna_action = BKE_action_add(&bmain, layered_action_name.c_str());
Action &converted_action = dna_action->wrap();
Slot &slot = converted_action.slot_add();
Layer &layer = converted_action.layer_add(legacy_action.id.name);
Strip &strip = layer.strip_add(converted_action, Strip::Type::Keyframe);
BLI_assert(strip.data<StripKeyframeData>(converted_action).channelbag_array_num == 0);
Channelbag *bag = &strip.data<StripKeyframeData>(converted_action).channelbag_for_slot_add(slot);
const int fcu_count = BLI_listbase_count(&legacy_action.curves);
bag->fcurve_array = MEM_calloc_arrayN<FCurve *>(fcu_count, "Convert to layered action");
bag->fcurve_array_num = fcu_count;
int i = 0;
blender::Map<FCurve *, FCurve *> old_new_fcurve_map;
LISTBASE_FOREACH_INDEX (FCurve *, fcu, &legacy_action.curves, i) {
bag->fcurve_array[i] = BKE_fcurve_copy(fcu);
bag->fcurve_array[i]->grp = nullptr;
old_new_fcurve_map.add(fcu, bag->fcurve_array[i]);
}
LISTBASE_FOREACH (bActionGroup *, group, &legacy_action.groups) {
/* The resulting group might not have the same name, because the legacy system allowed
* duplicate names while the new system ensures uniqueness. */
bActionGroup &converted_group = bag->channel_group_create(group->name);
LISTBASE_FOREACH (FCurve *, fcu, &group->channels) {
if (fcu->grp != group) {
/* Since the group listbase points to the action listbase, it won't stop iterating when
* reaching the end of the group but iterate to the end of the action FCurves. */
break;
}
FCurve *new_fcurve = old_new_fcurve_map.lookup(fcu);
bag->fcurve_assign_to_channel_group(*new_fcurve, converted_group);
}
}
return &converted_action;
}
/**
* Clone information from the given slot into this slot while retaining important info like the
* slot handle and runtime data. This copies the identifier which might clash with other
* identifiers on the action. Call `slot_identifier_ensure_unique` after.
*/
static void clone_slot(const Slot &from, Slot &to)
{
ActionSlotRuntimeHandle *runtime = to.runtime;
slot_handle_t handle = to.handle;
*reinterpret_cast<ActionSlot *>(&to) = *reinterpret_cast<const ActionSlot *>(&from);
to.runtime = runtime;
to.handle = handle;
}
void move_slot(Main &bmain, Slot &source_slot, Action &from_action, Action &to_action)
{
BLI_assert(from_action.slots().contains(&source_slot));
BLI_assert(&from_action != &to_action);
/* No merging of strips or layers is handled. All data is put into the assumed single strip. */
assert_baklava_phase_1_invariants(from_action);
assert_baklava_phase_1_invariants(to_action);
Slot &target_slot = to_action.slot_add();
clone_slot(source_slot, target_slot);
slot_identifier_ensure_unique(to_action, target_slot);
if (!from_action.layers().is_empty() && !from_action.layer(0)->strips().is_empty()) {
StripKeyframeData &from_strip_data = from_action.layer(0)->strip(0)->data<StripKeyframeData>(
from_action);
Channelbag *channelbag = from_strip_data.channelbag_for_slot(source_slot.handle);
/* It's perfectly fine for a slot to not have a channelbag on each keyframe strip. */
if (channelbag) {
/* Only create the layer & keyframe strip if there is a channelbag to move
* into it. Otherwise it's better to keep the Action lean, and defer their
* creation when keys are inserted. */
to_action.layer_keystrip_ensure();
StripKeyframeData &to_strip_data = to_action.layer(0)->strip(0)->data<StripKeyframeData>(
to_action);
channelbag->slot_handle = target_slot.handle;
grow_array_and_append<ActionChannelbag *>(
&to_strip_data.channelbag_array, &to_strip_data.channelbag_array_num, channelbag);
const int index = from_strip_data.find_channelbag_index(*channelbag);
shrink_array_and_remove<ActionChannelbag *>(
&from_strip_data.channelbag_array, &from_strip_data.channelbag_array_num, index);
}
}
/* Reassign all users of `source_slot` to the action `to_action` and the slot `target_slot`. */
for (ID *user : source_slot.users(bmain)) {
const auto assign_other_action = [&](ID & /* animated_id */,
bAction *&action_ptr_ref,
slot_handle_t &slot_handle_ref,
char *slot_identifier) -> bool {
/* Only reassign if the reference is actually from the same action. Could be from a different
* action when using the NLA or action constraints. */
if (action_ptr_ref != &from_action) {
return true;
}
{ /* Assign the Action. */
const bool assign_ok = generic_assign_action(
*user, &to_action, action_ptr_ref, slot_handle_ref, slot_identifier);
BLI_assert_msg(assign_ok, "Expecting slotted Actions to always be assignable");
UNUSED_VARS_NDEBUG(assign_ok);
}
{ /* Assign the Slot. */
const ActionSlotAssignmentResult result = generic_assign_action_slot(
&target_slot, *user, action_ptr_ref, slot_handle_ref, slot_identifier);
BLI_assert(result == ActionSlotAssignmentResult::OK);
UNUSED_VARS_NDEBUG(result);
}
/* TODO: move the tagging of animated IDs into generic_assign_action() and
* generic_assign_action_slot(), as that's closer to the modification of
* the animated ID.
*
* This line was added here for now, to fix #136388 with minimal impact on
* other code, so that the fix can be easily back-ported to Blender 4.4. */
DEG_id_tag_update(user, ID_RECALC_ANIMATION);
return true;
};
foreach_action_slot_use_with_references(*user, assign_other_action);
}
from_action.slot_remove(source_slot);
}
Slot &duplicate_slot(Action &action, const Slot &slot)
{
BLI_assert(action.slots().contains(const_cast<Slot *>(&slot)));
/* Duplicate the slot itself. */
Slot &cloned_slot = action.slot_add();
clone_slot(slot, cloned_slot);
slot_identifier_ensure_unique(action, cloned_slot);
/* Duplicate each Channelbag for the source slot. */
for (int i = 0; i < action.strip_keyframe_data_array_num; i++) {
StripKeyframeData &strip_data = action.strip_keyframe_data_array[i]->wrap();
strip_data.slot_data_duplicate(slot.handle, cloned_slot.handle);
}
/* The ID has changed, and so it needs to be re-evaluated. Animation does not
* have to be flushed since nothing is using this slot yet. */
DEG_id_tag_update(&action.id, ID_RECALC_ANIMATION_NO_FLUSH);
return cloned_slot;
}
} // namespace blender::animrig