WIP: Anim: Implement keyframing functionality for the Animation data-block #119669

Closed
Christoph Lendenfeld wants to merge 19 commits from ChrisLend/blender:baklava_keyframing into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
55 changed files with 4039 additions and 239 deletions

View File

@ -61,6 +61,7 @@ _modules = [
"properties_texture",
"properties_world",
"properties_collection",
"temp_anim_layers",
"generic_ui_list",
# Generic Space Modules
@ -121,6 +122,7 @@ def register():
register_class(cls)
space_filebrowser.register_props()
temp_anim_layers.register_props()
from bpy.props import (
EnumProperty,

View File

@ -2696,6 +2696,7 @@ class USERPREF_PT_experimental_prototypes(ExperimentalPanel, Panel):
({"property": "use_new_matrix_socket"}, ("blender/blender/issues/116067", "Matrix Socket")),
({"property": "enable_overlay_next"}, ("blender/blender/issues/102179", "#102179")),
({"property": "use_extension_repos"}, ("/blender/blender/issues/117286", "#117286")),
({"property": "use_animation_baklava"}, ("/blender/blender/pulls/114098", "#114098")),
),
)

View File

@ -0,0 +1,120 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""NOTE: this is temporary UI code to show animation layers.
It is not meant for any particular use, just to have *something* in the UI.
"""
import threading
import bpy
from bpy.types import Context, Panel, WindowManager
from bpy.props import PointerProperty
class VIEW3D_PT_animation_layers(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Animation"
bl_label = "Baklava"
@classmethod
def poll(cls, context: Context) -> bool:
return context.preferences.experimental.use_animation_baklava and context.object
def draw(self, context: Context) -> None:
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
# FIXME: this should be done in response to a messagebus callback, notifier, whatnot.
adt = context.object.animation_data
with _wm_selected_animation_lock:
if adt:
context.window_manager.selected_animation = adt.animation
else:
context.window_manager.selected_animation = None
col = layout.column()
# This has to go via an auxillary property, as assigning an Animation
# data-block should be possible even when `context.object.animation_data`
# is `None`, and thus its `animation` property does not exist.
col.template_ID(context.window_manager, 'selected_animation')
col = layout.column(align=True)
anim = adt and adt.animation
if anim:
col.prop(adt, 'animation_binding_handle', text="Binding")
binding = [o for o in anim.bindings if o.handle == adt.animation_binding_handle]
if binding:
col.prop(binding[0], 'name', text="Anim Binding Name")
else:
col.label(text="AN Binding Name: -")
if adt:
col.prop(adt, 'animation_binding_name', text="ADT Binding Name")
else:
col.label(text="ADT Binding Name: -")
layout.separator()
if not anim:
layout.label(text="No layers")
return
for layer_idx, layer in reversed(list(enumerate(anim.layers))):
layerbox = layout.box()
col = layerbox.column(align=True)
col.prop(layer, "name", text=f"Layer {layer_idx+1}:")
col.prop(layer, "influence")
col.prop(layer, "mix_mode")
classes = (
VIEW3D_PT_animation_layers,
)
_wm_selected_animation_lock = threading.Lock()
def _wm_selected_animation_update(self: WindowManager, context: Context) -> None:
# Avoid responding to changes written by the panel above.
lock_ok = _wm_selected_animation_lock.acquire(blocking=False)
if not lock_ok:
return
try:
if self.selected_animation is None and context.object.animation_data is None:
return
adt = context.object.animation_data_create()
if adt.animation == self.selected_animation:
# Avoid writing to the property when the new value hasn't changed.
return
adt.animation = self.selected_animation
finally:
_wm_selected_animation_lock.release()
def register_props() -> None:
# Put behind a `try` because it won't exist when Blender is built without
# experimental features.
try:
from bpy.types import Animation
except ImportError:
return
# Due to this hackyness, the WindowManager will increase the user count of
# the pointed-to Animation data-block.
WindowManager.selected_animation = PointerProperty(
type=Animation,
name="Animation",
description="Animation assigned to the active Object",
update=_wm_selected_animation_update,
)
if __name__ == "__main__": # only for live edit.
register_, _ = bpy.utils.register_classes_factory(classes)
register_()
register_props()

View File

@ -62,14 +62,92 @@ class Animation : public ::Animation {
const Layer *layer(int64_t index) const;
Layer *layer(int64_t index);
Layer &layer_add(StringRefNull name);
/**
* Remove the layer from this animation.
*
* After this call, the passed reference is no longer valid, as the memory
* will have been freed. Any strips on the layer will be freed too.
*
* \return true when the layer was found & removed, false if it wasn't found. */
bool layer_remove(Layer &layer_to_remove);
/* Animation Binding access. */
blender::Span<const Binding *> bindings() const;
blender::MutableSpan<Binding *> bindings();
const Binding *binding(int64_t index) const;
Binding *binding(int64_t index);
Binding *binding_for_handle(binding_handle_t handle);
const Binding *binding_for_handle(binding_handle_t handle) const;
/**
* Set the binding name.
*
* This has to be done on the Animation level to ensure each binding has a
* unique name within the Animation.
*
* \see Animation::binding_name_propagate
*/
void binding_name_set(Binding &binding, StringRefNull new_name);
/**
* Update the `AnimData::animation_binding_name` field of any ID that is animated by
* this.Binding.
*
* Should be called after `binding_name_set(binding)`. This is implemented as a separate function
* due to the need to access bmain, which is available in the RNA on-property-update handler, but
* not in the RNA property setter.
*/
void binding_name_propagate(Main &bmain, const Binding &binding);
Binding *binding_find_by_name(StringRefNull binding_name);
Binding *binding_for_id(const ID &animated_id);
const Binding *binding_for_id(const ID &animated_id) const;
Binding &binding_add();
/** Assign this animation to the ID.
*
* \param binding The binding this ID should be animated by, may be nullptr if it is to be
* assigned later. In that case, the ID will not actually receive any animation. \param
* animated_id The ID that should be animated by this Animation data-block.
*/
bool assign_id(Binding *binding, ID &animated_id);
void unassign_id(ID &animated_id);
/**
* Find the binding that best matches the animated ID.
*
* If the ID is already animated by this Animation, by matching this
* Animation's bindings with (in order):
*
* - `animated_id.adt->binding_handle`,
* - `animated_id.adt->binding_name`,
* - `animated_id.name`.
*
* Note that this different from #binding_for_id, which does not use the
* binding name, and only works when this Animation is already assigned. */
Binding *find_suitable_binding_for(const ID &animated_id);
/** Free all data in the `Animation`. Doesn't delete the `Animation` itself. */
void free_data();
/* Flags access. */
enum class Flag : uint8_t { Expanded = (1 << 0) };
bool is_expanded()
{
return this->flag & uint8_t(Flag::Expanded);
}
protected:
/** Return the layer's index, or -1 if not found in this animation. */
int64_t find_layer_index(const Layer &layer) const;
private:
Binding &binding_allocate();
};
static_assert(sizeof(Animation) == sizeof(::Animation),
"DNA struct and its C++ wrapper must have the same size");
@ -82,7 +160,17 @@ static_assert(sizeof(Animation) == sizeof(::Animation),
*/
class Strip : public ::AnimationStrip {
public:
Strip() = default;
/**
* Strip instances should not be created via this constructor. Create a sub-class like
* #KeyframeStrip instead.
*
* The reason is that various functions will assume that the `Strip` is actually a down-cast
* instance of another strip class, and that `Strip::type()` will say which type. To avoid having
* to explcitly deal with an 'invalid' type everywhere, creating a `Strip` directly is simply not
* allowed.
*/
Strip() = delete;
/**
* Strip cannot be duplicated via the copy constructor. Either use a concrete
* strip type's copy constructor, or use Strip::duplicate().
@ -117,6 +205,18 @@ class Strip : public ::AnimationStrip {
template<typename T> bool is() const;
template<typename T> T &as();
template<typename T> const T &as() const;
bool contains_frame(float frame_time) const;
bool is_last_frame(float frame_time) const;
/**
* Set the start and end frame.
*
* Note that this does not do anything else. There is no check whether the
* frame numbers are valid (i.e. frame_start <= frame_end). Infinite values
* (negative for frame_start, positive for frame_end) are supported.
*/
void resize(float frame_start, float frame_end);
};
static_assert(sizeof(Strip) == sizeof(::AnimationStrip),
"DNA struct and its C++ wrapper must have the same size");
@ -172,6 +272,20 @@ class Layer : public ::AnimationLayer {
blender::MutableSpan<Strip *> strips();
const Strip *strip(int64_t index) const;
Strip *strip(int64_t index);
Strip &strip_add(Strip::Type strip_type);
/**
* Remove the strip from this layer.
*
* After this call, the passed reference is no longer valid, as the memory
* will have been freed.
*
* \return true when the strip was found & removed, false if it wasn't found. */
bool strip_remove(Strip &strip);
protected:
/** Return the strip's index, or -1 if not found in this layer. */
int64_t find_strip_index(const Strip &strip) const;
};
static_assert(sizeof(Layer) == sizeof(::AnimationLayer),
"DNA struct and its C++ wrapper must have the same size");
@ -195,6 +309,27 @@ class Binding : public ::AnimationBinding {
Binding() = default;
Binding(const Binding &other) = default;
~Binding() = default;
/**
* Let the given ID receive animation from this binding.
*
* This is a low-level function; for most purposes you want
* #Animation::assign_id instead.
*
* \note This does _not_ set animated_id->adt->animation to the owner of this
* Binding. It's the caller's responsibility to do that.
*
* \return Whether this was possible. If the Binding was already bound to a
* specific ID type, and `animated_id` is of a different type, it will be
* refused. If the ID type cannot be animated at all, false is also returned.
*
* \see assign_animation
* \see Animation::assign_id
*/
bool connect_id(ID &animated_id);
/** Return whether this Binding is usable by this ID type. */
bool is_suitable_for(const ID &animated_id) const;
};
static_assert(sizeof(Binding) == sizeof(::AnimationBinding),
"DNA struct and its C++ wrapper must have the same size");
@ -213,6 +348,42 @@ class KeyframeStrip : public ::KeyframeAnimationStrip {
blender::MutableSpan<ChannelBag *> channelbags();
const ChannelBag *channelbag(int64_t index) const;
ChannelBag *channelbag(int64_t index);
/**
* Find the animation channels for this binding.
*
* \return nullptr if there is none yet for this binding.
*/
const ChannelBag *channelbag_for_binding(const Binding &binding) const;
ChannelBag *channelbag_for_binding(const Binding &binding);
const ChannelBag *channelbag_for_binding(binding_handle_t binding_handle) const;
ChannelBag *channelbag_for_binding(binding_handle_t binding_handle);
/**
* Add the animation channels for this binding.
*
* Should only be called when there is no `ChannelBag` for this binding yet.
*/
ChannelBag &channelbag_for_binding_add(const Binding &binding);
/**
* Find an FCurve for this binding + RNA path + array index combination.
*
* If it cannot be found, `nullptr` is returned.
*/
FCurve *fcurve_find(const Binding &binding, StringRefNull rna_path, int array_index);
/**
* Find an FCurve for this binding + RNA path + array index combination.
*
* If it cannot be found, a new one is created.
*/
FCurve &fcurve_find_or_create(const Binding &binding, StringRefNull rna_path, int array_index);
FCurve *keyframe_insert(const Binding &binding,
StringRefNull rna_path,
int array_index,
float2 time_value,
const KeyframeSettings &settings);
};
static_assert(sizeof(KeyframeStrip) == sizeof(::KeyframeAnimationStrip),
"DNA struct and its C++ wrapper must have the same size");
@ -234,10 +405,54 @@ class ChannelBag : public ::AnimationChannelBag {
blender::MutableSpan<FCurve *> fcurves();
const FCurve *fcurve(int64_t index) const;
FCurve *fcurve(int64_t index);
const FCurve *fcurve_find(const StringRefNull rna_path, const int array_index) const;
};
static_assert(sizeof(ChannelBag) == sizeof(::AnimationChannelBag),
"DNA struct and its C++ wrapper must have the same size");
/**
* Assign the animation to the ID.
*
* This will will make a best-effort guess as to which binding to use, in this
* order;
*
* - By stable index.
* - By fallback string.
* - By the ID's name (matching agains the binding name).
* - If the above do not find a suitable binding, the animated ID will not
* receive any animation and the calller is responsible for creating an binding
* and assigning it.
*
* \return `false` if the assignment was not possible (for example the ID is of a type that cannot
* be animated). If the above fall-through case of "no binding found" is reached, this function
* will still return `true` as the Animation was succesfully assigned.
*/
bool assign_animation(Animation &anim, ID &animated_id);
/**
* Ensure that this ID is no longer animated.
*/
void unassign_animation(ID &animated_id);
/**
* Return the Animation of this ID, or nullptr if it has none.
*/
Animation *get_animation(ID &animated_id);
/**
* Return the F-Curves for this specific binding handle.
*
* This is just a utility function, that's intended to become obsolete when multi-layer animation
* is introduced. However, since Blender currently only supports a single layer with a single
* strip, of a single type, this function can be used.
*
* The use of this function is also an indicator for code that will have to be altered when
* multi-layered animation is getting implemented.
*/
Span<FCurve *> fcurves_for_animation(Animation &anim, binding_handle_t binding_handle);
Span<const FCurve *> fcurves_for_animation(const Animation &anim, binding_handle_t binding_handle);
} // namespace blender::animrig
/* Wrap functions for the DNA structs. */

View File

@ -10,6 +10,8 @@
#pragma once
#include "BLI_string_ref.hh"
struct ID;
struct Main;
@ -20,12 +22,19 @@ struct bAction;
namespace blender::animrig {
class Animation;
/**
* Get (or add relevant data to be able to do so) the Active Action for the given
* Animation Data block, given an ID block where the Animation Data should reside.
*/
bAction *id_action_ensure(Main *bmain, ID *id);
/**
* Get or create the animation datablock for the given ID. Doesn NOT create a binding.
*/
Animation *id_animation_ensure(Main *bmain, ID *id);
/**
* Delete the F-Curve from the given AnimData block (if possible),
* as appropriate according to animation context.
@ -44,4 +53,27 @@ void reevaluate_fcurve_errors(bAnimContext *ac);
*/
bool animdata_remove_empty_action(AnimData *adt);
/**
* Compatibility helper function for `BKE_animadata_fcurve_find_by_rna_path()`.
*
* Searches each layer (top to bottom) to find an FCurve that matches the given
* RNA path & index.
*
* \see BKE_animadata_fcurve_find_by_rna_path
*
* \note The returned FCurve should NOT be used for keyframe manipulation. Its
* existence is an indicator for "this property is animated".
*
* This function should probably be limited to the active layer (for the given
* property, once pinning to layers is there), so that the "this is keyed" color
* is more accurate.
*
* Again, this is just to hook up the new Animation data-block to the old
* Blender UI code.
*/
const FCurve *fcurve_find_by_rna_path(const Animation &anim,
const ID &animated_id,
StringRefNull rna_path,
int array_index);
} // namespace blender::animrig

View File

@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2023 Blender Developers
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Animation data-block evaluation.
*/
#pragma once
#include "DNA_anim_types.h"
#include "ANIM_animation.hh"
struct AnimationEvalContext;
struct PointerRNA;
namespace blender::animrig {
/**
* Top level animation evaluation function.
*
* Animate the given ID, using the animation data-block and the given binding.
*
* \param flush_to_original when true, look up the original data-block (assuming
* the given one is an evaluated copy) and update that too.
*/
void evaluate_and_apply_animation(PointerRNA &animated_id_ptr,
Animation &animation,
binding_handle_t binding_handle,
const AnimationEvalContext &anim_eval_context,
bool flush_to_original);
} // namespace blender::animrig

View File

@ -10,7 +10,10 @@
#pragma once
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "DNA_anim_types.h"
struct AnimData;
struct FCurve;
@ -31,6 +34,11 @@ struct KeyframeSettings {
*/
KeyframeSettings get_keyframe_settings(bool from_userprefs);
/**
* Create an fcurve for a specific channel, pre-set-up with default flags and interpolation mode.
*/
FCurve *create_fcurve_for_channel(StringRef rna_path, int array_index);
/** Initialize the given BezTriple with default values. */
void initialize_bezt(BezTriple *beztr,
float2 position,

View File

@ -27,6 +27,7 @@ set(SRC
intern/bone_collections.cc
intern/bonecolor.cc
intern/driver.cc
intern/evaluation.cc
intern/fcurve.cc
intern/keyframing.cc
intern/keyframing_auto.cc
@ -39,11 +40,13 @@ set(SRC
ANIM_bone_collections.hh
ANIM_bonecolor.hh
ANIM_driver.hh
ANIM_evaluation.hh
ANIM_fcurve.hh
ANIM_keyframing.hh
ANIM_rna.hh
ANIM_visualkey.hh
intern/bone_collections_internal.hh
intern/evaluation_internal.hh
)
set(LIB
@ -54,6 +57,7 @@ set(LIB
PRIVATE bf_editor_interface
PRIVATE bf::intern::guardedalloc
PRIVATE bf::intern::atomic
PRIVATE bf::intern::clog
)
@ -64,7 +68,9 @@ if(WITH_GTESTS)
set(TEST_INC
)
set(TEST_SRC
intern/animation_test.cc
intern/bone_collections_test.cc
intern/evaluation_test.cc
)
set(TEST_LIB
PRIVATE bf::animrig

View File

@ -7,6 +7,7 @@
*/
#include "ANIM_action.hh"
#include "ANIM_fcurve.hh"
#include "BKE_action.h"
#include "BKE_fcurve.hh"
#include "BLI_listbase.h"
@ -49,17 +50,12 @@ FCurve *action_fcurve_ensure(Main *bmain,
return fcu;
}
fcu = BKE_fcurve_create();
fcu = create_fcurve_for_channel(rna_path, array_index);
fcu->flag = (FCURVE_VISIBLE | FCURVE_SELECTED);
fcu->auto_smoothing = U.auto_smoothing_new;
if (BLI_listbase_is_empty(&act->curves)) {
fcu->flag |= FCURVE_ACTIVE;
}
fcu->rna_path = BLI_strdup(rna_path);
fcu->array_index = array_index;
if (U.keying_flag & KEYING_FLAG_XYZ2RGB && ptr != nullptr) {
/* For Loc/Rot/Scale and also Color F-Curves, the color of the F-Curve in the Graph Editor,
* is determined by the array index for the F-Curve.

View File

@ -4,6 +4,7 @@
#include "DNA_anim_defaults.h"
#include "DNA_anim_types.h"
#include "DNA_array_utils.hh"
#include "DNA_defaults.h"
#include "BLI_listbase.h"
@ -33,6 +34,67 @@
namespace blender::animrig {
static animrig::Layer &animationlayer_alloc()
{
AnimationLayer *layer = DNA_struct_default_alloc(AnimationLayer);
return layer->wrap();
}
static animrig::Strip &animationstrip_alloc_infinite(const Strip::Type type)
{
AnimationStrip *strip;
switch (type) {
case Strip::Type::Keyframe: {
KeyframeAnimationStrip *key_strip = MEM_new<KeyframeAnimationStrip>(__func__);
strip = &key_strip->strip;
break;
}
}
BLI_assert_msg(strip, "unsupported strip type");
/* Copy the default AnimationStrip fields into the allocated data-block. */
memcpy(strip, DNA_struct_default_get(AnimationStrip), sizeof(*strip));
return strip->wrap();
}
/* Copied from source/blender/blenkernel/intern/grease_pencil.cc. It also has a shrink_array()
* function, if we ever need one (we will).
* 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 = reinterpret_cast<T *>(
MEM_cnew_array<T *>(new_array_num, "animrig::animation/grow_array"));
blender::uninitialized_relocate_n(*array, *num, new_array);
if (*array != nullptr) {
MEM_freeN(*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 shrink_array(T **array, int *num, const int shrink_num)
{
BLI_assert(shrink_num > 0);
const int new_array_num = *num - shrink_num;
T *new_array = reinterpret_cast<T *>(MEM_cnew_array<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;
}
/* ----- Animation implementation ----------- */
blender::Span<const Layer *> Animation::layers() const
@ -54,6 +116,52 @@ Layer *Animation::layer(const int64_t index)
return &this->layer_array[index]->wrap();
}
Layer &Animation::layer_add(const StringRefNull name)
{
using namespace blender::animrig;
Layer &new_layer = animationlayer_alloc();
STRNCPY_UTF8(new_layer.name, name.c_str());
grow_array_and_append<::AnimationLayer *>(
&this->layer_array, &this->layer_array_num, &new_layer);
this->layer_active_index = this->layer_array_num - 1;
return new_layer;
}
static void layer_ptr_destructor(AnimationLayer **dna_layer_ptr)
{
Layer &layer = (*dna_layer_ptr)->wrap();
MEM_delete(&layer);
};
bool Animation::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;
}
int64_t Animation::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;
}
blender::Span<const Binding *> Animation::bindings() const
{
return blender::Span<Binding *>{reinterpret_cast<Binding **>(this->binding_array),
@ -73,6 +181,169 @@ Binding *Animation::binding(const int64_t index)
return &this->binding_array[index]->wrap();
}
Binding *Animation::binding_for_handle(const binding_handle_t handle)
{
const Binding *binding = const_cast<const Animation *>(this)->binding_for_handle(handle);
return const_cast<Binding *>(binding);
}
const Binding *Animation::binding_for_handle(const binding_handle_t handle) const
{
/* TODO: implement hashmap lookup. */
for (const Binding *binding : bindings()) {
if (binding->handle == handle) {
return binding;
}
}
return nullptr;
}
static void anim_binding_name_ensure_unique(Animation &animation, Binding &binding)
{
/* Cannot capture parameters by reference in the lambda, as that would change its signature
* and no longer be compatible with BLI_uniquename_cb(). That's why this struct is necessary. */
struct DupNameCheckData {
Animation &anim;
Binding &binding;
};
DupNameCheckData check_data = {animation, binding};
auto check_name_is_used = [](void *arg, const char *name) -> bool {
DupNameCheckData *data = static_cast<DupNameCheckData *>(arg);
for (const Binding *binding : data->anim.bindings()) {
if (binding == &data->binding) {
/* Don't compare against the binding that's being renamed. */
continue;
}
if (STREQ(binding->name, name)) {
return true;
}
}
return false;
};
BLI_uniquename_cb(check_name_is_used, &check_data, "", '.', binding.name, sizeof(binding.name));
}
void Animation::binding_name_set(Binding &binding, const StringRefNull new_name)
{
STRNCPY_UTF8(binding.name, new_name.c_str());
anim_binding_name_ensure_unique(*this, binding);
}
void Animation::binding_name_propagate(Main &bmain, const Binding &binding)
{
/* Just loop over all animatable IDs in the main dataabase. */
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->animation != this) {
/* Not animated by this Animation. */
continue;
}
if (adt->binding_handle != binding.handle) {
/* Not animated by this Binding. */
continue;
}
/* Ensure the Binding name on the AnimData is correct. */
STRNCPY_UTF8(adt->binding_name, binding.name);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
FOREACH_MAIN_LISTBASE_END;
}
Binding *Animation::binding_find_by_name(const StringRefNull binding_name)
{
for (Binding *binding : bindings()) {
if (STREQ(binding->name, binding_name.c_str())) {
return binding;
}
}
return nullptr;
}
Binding *Animation::binding_for_id(const ID &animated_id)
{
const Binding *binding = const_cast<const Animation *>(this)->binding_for_id(animated_id);
return const_cast<Binding *>(binding);
}
const Binding *Animation::binding_for_id(const ID &animated_id) const
{
const AnimData *adt = BKE_animdata_from_id(&animated_id);
/* Note that there is no check that `adt->animation` is actually `this`. */
const Binding *binding = this->binding_for_handle(adt->binding_handle);
if (!binding) {
return nullptr;
}
if (!binding->is_suitable_for(animated_id)) {
return nullptr;
}
return binding;
}
Binding &Animation::binding_allocate()
{
Binding &binding = MEM_new<AnimationBinding>(__func__)->wrap();
this->last_binding_handle++;
BLI_assert_msg(this->last_binding_handle > 0, "Animation Binding handle 32-bit overflow");
binding.handle = this->last_binding_handle;
return binding;
}
Binding &Animation::binding_add()
{
Binding &binding = this->binding_allocate();
/* Append the Binding to the animation data-block. */
grow_array_and_append<::AnimationBinding *>(
&this->binding_array, &this->binding_array_num, &binding);
return binding;
}
Binding *Animation::find_suitable_binding_for(const ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
/* The stable index is only valid when this animation has already been
* assigned. Otherwise it's meaningless. */
if (adt && adt->animation == this) {
Binding *binding = this->binding_for_handle(adt->binding_handle);
if (binding && binding->is_suitable_for(animated_id)) {
return binding;
}
}
/* Try the binding name from the AnimData, if it is set,*/
if (adt && adt->binding_name[0]) {
Binding *binding = this->binding_find_by_name(adt->binding_name);
if (binding && binding->is_suitable_for(animated_id)) {
return binding;
}
}
/* As a last resort, search for the ID name. */
Binding *binding = this->binding_find_by_name(animated_id.name);
if (binding && binding->is_suitable_for(animated_id)) {
return binding;
}
return nullptr;
}
void Animation::free_data()
{
/* Free layers. */
@ -90,6 +361,62 @@ void Animation::free_data()
this->binding_array_num = 0;
}
bool Animation::assign_id(Binding *binding, ID &animated_id)
{
AnimData *adt = BKE_animdata_ensure_id(&animated_id);
if (!adt) {
return false;
}
if (adt->animation) {
/* Unassign the ID from its existing animation first, or use the top-level
* function `assign_animation(anim, ID)`. */
return false;
}
if (binding) {
if (!binding->connect_id(animated_id)) {
return false;
}
/* If the binding is not yet named, use the ID name. */
if (binding->name[0] == '\0') {
this->binding_name_set(*binding, animated_id.name);
}
/* Always make sure the ID's binding name matches the assigned binding. */
STRNCPY_UTF8(adt->binding_name, binding->name);
}
else {
adt->binding_handle = 0;
/* Keep adt->binding_name untouched, as A) it's not necessary to erase it
* because `adt->binding_handle = 0` already indicates "no binding yet",
* and B) it would erase information that can later be used when trying to
* identify which binding this was once attached to. */
}
adt->animation = this;
id_us_plus(&this->id);
return true;
}
void Animation::unassign_id(ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
BLI_assert_msg(adt->animation == this, "ID is not assigned to this Animation");
/* Before unassigning, make sure that the stored Binding name is up to date.
* If Blender would be bug-free, and we could assume that `Animation::binding_name_propagate()`
* would always be called when appropriate, this code could be removed. */
const Binding *binding = this->binding_for_handle(adt->binding_handle);
if (binding) {
STRNCPY_UTF8(adt->binding_name, binding->name);
}
id_us_min(&this->id);
adt->animation = nullptr;
}
/* ----- AnimationLayer implementation ----------- */
Layer::Layer(const Layer &other)
@ -131,7 +458,101 @@ Strip *Layer::strip(const int64_t index)
return &this->strip_array[index]->wrap();
}
Strip &Layer::strip_add(const Strip::Type strip_type)
{
Strip &strip = animationstrip_alloc_infinite(strip_type);
/* Add the new strip to the strip array. */
grow_array_and_append<::AnimationStrip *>(&this->strip_array, &this->strip_array_num, &strip);
return strip;
}
static void strip_ptr_destructor(AnimationStrip **dna_strip_ptr)
{
Strip &strip = (*dna_strip_ptr)->wrap();
MEM_delete(&strip);
};
bool Layer::strip_remove(Strip &strip_to_remove)
{
const int64_t strip_index = this->find_strip_index(strip_to_remove);
if (strip_index < 0) {
return false;
}
dna::array::remove_index(
&this->strip_array, &this->strip_array_num, nullptr, strip_index, strip_ptr_destructor);
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;
}
/* ----- AnimationBinding implementation ----------- */
bool Binding::connect_id(ID &animated_id)
{
if (!this->is_suitable_for(animated_id)) {
return false;
}
AnimData *adt = BKE_animdata_ensure_id(&animated_id);
if (!adt) {
return false;
}
if (this->idtype == 0) {
this->idtype = GS(animated_id.name);
}
adt->binding_handle = this->handle;
return true;
}
bool Binding::is_suitable_for(const ID &animated_id) const
{
/* Check that the ID type is compatible with this binding. */
const int animated_idtype = GS(animated_id.name);
return this->idtype == 0 || this->idtype == animated_idtype;
}
bool assign_animation(Animation &anim, ID &animated_id)
{
unassign_animation(animated_id);
Binding *binding = anim.find_suitable_binding_for(animated_id);
return anim.assign_id(binding, animated_id);
}
void unassign_animation(ID &animated_id)
{
Animation *anim = get_animation(animated_id);
if (!anim) {
return;
}
anim->unassign_id(animated_id);
}
Animation *get_animation(ID &animated_id)
{
AnimData *adt = BKE_animdata_from_id(&animated_id);
if (!adt) {
return nullptr;
}
if (!adt->animation) {
return nullptr;
}
return &adt->animation->wrap();
}
/* ----- AnimationStrip implementation ----------- */
@ -158,6 +579,29 @@ Strip::~Strip()
BLI_assert_unreachable();
}
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;
}
/* ----- KeyframeAnimationStrip implementation ----------- */
KeyframeStrip::KeyframeStrip(const KeyframeStrip &other)
@ -216,6 +660,123 @@ ChannelBag *KeyframeStrip::channelbag(const int64_t index)
{
return &this->channelbags_array[index]->wrap();
}
const ChannelBag *KeyframeStrip::channelbag_for_binding(
const binding_handle_t binding_handle) const
{
for (const ChannelBag *channels : this->channelbags()) {
if (channels->binding_handle == binding_handle) {
return channels;
}
}
return nullptr;
}
ChannelBag *KeyframeStrip::channelbag_for_binding(const binding_handle_t binding_handle)
{
const auto *const_this = const_cast<const KeyframeStrip *>(this);
const auto *const_channels = const_this->channelbag_for_binding(binding_handle);
return const_cast<ChannelBag *>(const_channels);
}
const ChannelBag *KeyframeStrip::channelbag_for_binding(const Binding &binding) const
{
return this->channelbag_for_binding(binding.handle);
}
ChannelBag *KeyframeStrip::channelbag_for_binding(const Binding &binding)
{
return this->channelbag_for_binding(binding.handle);
}
ChannelBag &KeyframeStrip::channelbag_for_binding_add(const Binding &binding)
{
#ifndef NDEBUG
BLI_assert_msg(channelbag_for_binding(binding) == nullptr,
"Cannot add chans-for-binding for already-registered binding");
#endif
ChannelBag &channels = MEM_new<AnimationChannelBag>(__func__)->wrap();
channels.binding_handle = binding.handle;
grow_array_and_append<AnimationChannelBag *>(
&this->channelbags_array, &this->channelbags_array_num, &channels);
return channels;
}
FCurve *KeyframeStrip::fcurve_find(const Binding &binding,
const StringRefNull rna_path,
const int array_index)
{
ChannelBag *channels = this->channelbag_for_binding(binding);
if (channels == nullptr) {
return nullptr;
}
/* Copy of the logic in BKE_fcurve_find(), but then compatible with our array-of-FCurves
* instead of ListBase. */
for (FCurve *fcu : channels->fcurves()) {
/* Check indices first, much cheaper than a string comparison. */
/* Simple string-compare (this assumes that they have the same root...) */
if (fcu->array_index == array_index && fcu->rna_path && StringRef(fcu->rna_path) == rna_path) {
return fcu;
}
}
return nullptr;
}
FCurve &KeyframeStrip::fcurve_find_or_create(const Binding &binding,
const StringRefNull rna_path,
const int array_index)
{
if (FCurve *existing_fcurve = this->fcurve_find(binding, rna_path, array_index)) {
return *existing_fcurve;
}
FCurve *new_fcurve = create_fcurve_for_channel(rna_path.c_str(), array_index);
ChannelBag *channels = this->channelbag_for_binding(binding);
if (channels == nullptr) {
channels = &this->channelbag_for_binding_add(binding);
}
if (channels->fcurve_array_num == 0) {
new_fcurve->flag |= FCURVE_ACTIVE; /* First curve is added active. */
}
grow_array_and_append(&channels->fcurve_array, &channels->fcurve_array_num, new_fcurve);
return *new_fcurve;
}
FCurve *KeyframeStrip::keyframe_insert(const Binding &binding,
const StringRefNull rna_path,
const int array_index,
const float2 time_value,
const KeyframeSettings &settings)
{
FCurve &fcurve = this->fcurve_find_or_create(binding, rna_path, array_index);
if (!BKE_fcurve_is_keyframable(&fcurve)) {
/* TODO: handle this properly, in a way that can be communicated to the user. */
std::fprintf(stderr,
"FCurve %s[%d] for binding %s doesn't allow inserting keys.\n",
rna_path.c_str(),
array_index,
binding.name);
return nullptr;
}
/* TODO: Handle the eInsertKeyFlags. */
const int index = insert_vert_fcurve(&fcurve, time_value, settings, eInsertKeyFlags(0));
if (index < 0) {
std::fprintf(stderr,
"Could not insert key into FCurve %s[%d] for binding %s.\n",
rna_path.c_str(),
array_index,
binding.name);
return nullptr;
}
return &fcurve;
}
/* AnimationChannelBag implementation. */
@ -257,4 +818,49 @@ FCurve *ChannelBag::fcurve(const int64_t index)
return this->fcurve_array[index];
}
const FCurve *ChannelBag::fcurve_find(const StringRefNull rna_path, const int array_index) const
{
for (const FCurve *fcu : this->fcurves()) {
/* Check indices first, much cheaper than a string comparison. */
if (fcu->array_index == array_index && fcu->rna_path && StringRef(fcu->rna_path) == rna_path) {
return fcu;
}
}
return nullptr;
}
Span<FCurve *> fcurves_for_animation(Animation &anim, const binding_handle_t binding_handle)
{
const Span<const FCurve *> fcurves = fcurves_for_animation(const_cast<const Animation &>(anim),
binding_handle);
FCurve **first = const_cast<FCurve **>(fcurves.data());
return Span<FCurve *>(first, fcurves.size());
}
Span<const FCurve *> fcurves_for_animation(const Animation &anim,
const binding_handle_t binding_handle)
{
if (binding_handle == 0) {