diff --git a/scripts/startup/bl_ui/space_userpref.py b/scripts/startup/bl_ui/space_userpref.py index 4225373c4c2..7de770c610f 100644 --- a/scripts/startup/bl_ui/space_userpref.py +++ b/scripts/startup/bl_ui/space_userpref.py @@ -2680,6 +2680,7 @@ class USERPREF_PT_experimental_prototypes(ExperimentalPanel, Panel): ({"property": "enable_overlay_next"}, ("blender/blender/issues/102179", "#102179")), ({"property": "use_extension_repos"}, ("/blender/blender/issues/117286", "#117286")), ({"property": "use_extension_utils"}, ("/blender/blender/issues/117286", "#117286")), + ({"property": "use_animation_baklava"}, ("/blender/blender/pulls/114098", "#114098")), ), ) diff --git a/source/blender/animrig/ANIM_animation.hh b/source/blender/animrig/ANIM_animation.hh index 83ca4f0460d..eaa43af1561 100644 --- a/source/blender/animrig/ANIM_animation.hh +++ b/source/blender/animrig/ANIM_animation.hh @@ -62,14 +62,73 @@ 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 bindings() const; blender::MutableSpan 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_define + * \see Animation::binding_name_propagate + */ + void binding_name_set(Main &bmain, Binding &binding, StringRefNull new_name); + + /** + * Set the binding name, and ensure it is unique. + * + * This function usually isn't necessary, call #binding_name_set instead. + * + * \see Animation::binding_name_set + * \see Animation::binding_name_propagate + */ + void binding_name_define(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_define(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(); /** Free all data in the `Animation`. Doesn't delete the `Animation` itself. */ void free_data(); + + 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"); @@ -172,6 +231,21 @@ class Layer : public ::AnimationLayer { blender::MutableSpan 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 +269,9 @@ class Binding : public ::AnimationBinding { Binding() = default; Binding(const Binding &other) = default; ~Binding() = default; + + /** 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 +290,23 @@ class KeyframeStrip : public ::KeyframeAnimationStrip { blender::MutableSpan 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); }; static_assert(sizeof(KeyframeStrip) == sizeof(::KeyframeAnimationStrip), "DNA struct and its C++ wrapper must have the same size"); diff --git a/source/blender/animrig/CMakeLists.txt b/source/blender/animrig/CMakeLists.txt index d8ed65d6176..25974854f11 100644 --- a/source/blender/animrig/CMakeLists.txt +++ b/source/blender/animrig/CMakeLists.txt @@ -54,6 +54,7 @@ set(LIB PRIVATE bf_editor_interface PRIVATE bf::intern::guardedalloc PRIVATE bf::intern::atomic + PRIVATE bf::intern::clog ) @@ -64,6 +65,7 @@ if(WITH_GTESTS) set(TEST_INC ) set(TEST_SRC + intern/animation_test.cc intern/bone_collections_test.cc ) set(TEST_LIB diff --git a/source/blender/animrig/intern/animation.cc b/source/blender/animrig/intern/animation.cc index e026c0701db..1a691ca8e95 100644 --- a/source/blender/animrig/intern/animation.cc +++ b/source/blender/animrig/intern/animation.cc @@ -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,64 @@ 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 = nullptr; + switch (type) { + case Strip::Type::Keyframe: { + KeyframeAnimationStrip *key_strip = MEM_new(__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. + * Keep an eye on DNA_array_utils.hh; we may want to move these functions in there. */ +template 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( + MEM_cnew_array(new_array_num, "animrig::animation/grow_array")); + + blender::uninitialized_relocate_n(*array, *num, new_array); + MEM_SAFE_FREE(*array); + + *array = new_array; + *num = new_array_num; +} + +template static void grow_array_and_append(T **array, int *num, T item) +{ + grow_array(array, num, 1); + (*array)[*num - 1] = item; +} + +template 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(MEM_cnew_array(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 Animation::layers() const @@ -54,6 +113,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 Animation::bindings() const { return blender::Span{reinterpret_cast(this->binding_array), @@ -73,6 +178,145 @@ 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(this)->binding_for_handle(handle); + return const_cast(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(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(Main &bmain, Binding &binding, const StringRefNull new_name) +{ + this->binding_name_define(binding, new_name); + this->binding_name_propagate(bmain, binding); +} + +void Animation::binding_name_define(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(this)->binding_for_id(animated_id); + return const_cast(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(__func__)->wrap(); + this->last_binding_handle++; + BLI_assert_msg(this->last_binding_handle > 0, "Animation Binding handle 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; +} + void Animation::free_data() { /* Free layers. */ @@ -131,8 +375,55 @@ 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::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; +} + /* ----- AnimationStrip implementation ----------- */ Strip *Strip::duplicate(const StringRefNull allocation_name) const @@ -216,6 +507,46 @@ 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(this); + const auto *const_channels = const_this->channelbag_for_binding(binding_handle); + return const_cast(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(__func__)->wrap(); + channels.binding_handle = binding.handle; + + grow_array_and_append( + &this->channelbags_array, &this->channelbags_array_num, &channels); + + return channels; +} /* AnimationChannelBag implementation. */ diff --git a/source/blender/animrig/intern/animation_test.cc b/source/blender/animrig/intern/animation_test.cc new file mode 100644 index 00000000000..4e157b035b5 --- /dev/null +++ b/source/blender/animrig/intern/animation_test.cc @@ -0,0 +1,178 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "ANIM_animation.hh" + +#include "BKE_anim_data.hh" +#include "BKE_animation.hh" +#include "BKE_fcurve.hh" +#include "BKE_idtype.hh" +#include "BKE_lib_id.hh" +#include "BKE_main.hh" +#include "BKE_object.hh" + +#include "DNA_anim_types.h" +#include "DNA_object_types.h" + +#include "BLI_listbase.h" +#include "BLI_string_utf8.h" + +#include + +#include "CLG_log.h" +#include "testing/testing.h" + +namespace blender::animrig::tests { +class AnimationLayersTest : public testing::Test { + public: + Main *bmain; + Animation *anim; + Object *cube; + Object *suzanne; + + static void SetUpTestSuite() + { + /* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialised properly. */ + CLG_init(); + + /* To make id_can_have_animdata() and friends work, the `id_types` array needs to be set up. */ + BKE_idtype_init(); + } + + static void TearDownTestSuite() + { + CLG_exit(); + } + + void SetUp() override + { + bmain = BKE_main_new(); + anim = static_cast(BKE_id_new(bmain, ID_AN, "ANÄnimåtië")); + cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus"); + suzanne = BKE_object_add_only_object(bmain, OB_EMPTY, "OBSuzanne"); + } + + void TearDown() override + { + BKE_main_free(bmain); + } +}; + +TEST_F(AnimationLayersTest, add_layer) +{ + Layer &layer = anim->layer_add("layer name"); + + EXPECT_EQ(anim->layer(0), &layer); + EXPECT_EQ("layer name", std::string(layer.name)); + EXPECT_EQ(1.0f, layer.influence) << "Expected DNA defaults to be used."; + EXPECT_EQ(0, anim->layer_active_index) + << "Expected newly added layer to become the active layer."; + ASSERT_EQ(0, layer.strips().size()) << "Expected newly added layer to have no strip."; +} + +TEST_F(AnimationLayersTest, remove_layer) +{ + Layer &layer0 = anim->layer_add("Test Læür nul"); + Layer &layer1 = anim->layer_add("Test Læür één"); + Layer &layer2 = anim->layer_add("Test Læür twee"); + + /* Add some strips to check that they are freed correctly too (implicitly by the + * memory leak checker). */ + layer0.strip_add(Strip::Type::Keyframe); + layer1.strip_add(Strip::Type::Keyframe); + layer2.strip_add(Strip::Type::Keyframe); + + { /* Test removing a layer that is not owned. */ + Animation *other_anim = static_cast(BKE_id_new(bmain, ID_AN, "ANOtherAnim")); + Layer &other_layer = other_anim->layer_add("Another Layer"); + EXPECT_FALSE(anim->layer_remove(other_layer)) + << "Removing a layer not owned by the animation should be gracefully rejected"; + BKE_id_free(bmain, &other_anim->id); + } + + EXPECT_TRUE(anim->layer_remove(layer1)); + EXPECT_EQ(2, anim->layers().size()); + EXPECT_STREQ(layer0.name, anim->layer(0)->name); + EXPECT_STREQ(layer2.name, anim->layer(1)->name); + + EXPECT_TRUE(anim->layer_remove(layer2)); + EXPECT_EQ(1, anim->layers().size()); + EXPECT_STREQ(layer0.name, anim->layer(0)->name); + + EXPECT_TRUE(anim->layer_remove(layer0)); + EXPECT_EQ(0, anim->layers().size()); +} + +TEST_F(AnimationLayersTest, add_strip) +{ + Layer &layer = anim->layer_add("Test Læür"); + + Strip &strip = layer.strip_add(Strip::Type::Keyframe); + ASSERT_EQ(1, layer.strips().size()); + EXPECT_EQ(&strip, layer.strip(0)); + + constexpr float inf = std::numeric_limits::infinity(); + EXPECT_EQ(-inf, strip.frame_start) << "Expected strip to be infinite."; + EXPECT_EQ(inf, strip.frame_end) << "Expected strip to be infinite."; + EXPECT_EQ(0, strip.frame_offset) << "Expected infinite strip to have no offset."; + + Strip &another_strip = layer.strip_add(Strip::Type::Keyframe); + ASSERT_EQ(2, layer.strips().size()); + EXPECT_EQ(&another_strip, layer.strip(1)); + + EXPECT_EQ(-inf, another_strip.frame_start) << "Expected strip to be infinite."; + EXPECT_EQ(inf, another_strip.frame_end) << "Expected strip to be infinite."; + EXPECT_EQ(0, another_strip.frame_offset) << "Expected infinite strip to have no offset."; +} + +TEST_F(AnimationLayersTest, remove_strip) +{ + Layer &layer = anim->layer_add("Test Læür"); + Strip &strip0 = layer.strip_add(Strip::Type::Keyframe); + Strip &strip1 = layer.strip_add(Strip::Type::Keyframe); + Strip &strip2 = layer.strip_add(Strip::Type::Keyframe); + + EXPECT_TRUE(layer.strip_remove(strip1)); + EXPECT_EQ(2, layer.strips().size()); + EXPECT_EQ(&strip0, layer.strip(0)); + EXPECT_EQ(&strip2, layer.strip(1)); + + EXPECT_TRUE(layer.strip_remove(strip2)); + EXPECT_EQ(1, layer.strips().size()); + EXPECT_EQ(&strip0, layer.strip(0)); + + EXPECT_TRUE(layer.strip_remove(strip0)); + EXPECT_EQ(0, layer.strips().size()); + + { /* Test removing a strip that is not owned. */ + Layer &other_layer = anim->layer_add("Another Layer"); + Strip &other_strip = other_layer.strip_add(Strip::Type::Keyframe); + + EXPECT_FALSE(layer.strip_remove(other_strip)) + << "Removing a strip not owned by the layer should be gracefully rejected"; + } +} + +TEST_F(AnimationLayersTest, add_binding) +{ + Binding &binding = anim->binding_add(); + EXPECT_EQ(1, anim->last_binding_handle); + EXPECT_EQ(1, binding.handle); + + EXPECT_STREQ("", binding.name); + EXPECT_EQ(0, binding.idtype); +} + +TEST_F(AnimationLayersTest, rename_binding_name_collision) +{ + Binding &binding1 = anim->binding_add(); + Binding &binding2 = anim->binding_add(); + + anim->binding_name_define(binding1, "New Binding Name"); + anim->binding_name_define(binding2, "New Binding Name"); + EXPECT_STREQ("New Binding Name", binding1.name); + EXPECT_STREQ("New Binding Name.001", binding2.name); +} + +} // namespace blender::animrig::tests diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 2490671feac..4cbeaf557f9 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -723,8 +723,8 @@ typedef struct UserDef_Experimental { char use_extension_repos; char use_extension_utils; char use_grease_pencil_version3_convert_on_load; + char use_animation_baklava; - char _pad[1]; /** `makesdna` does not allow empty structs. */ } UserDef_Experimental; diff --git a/source/blender/makesrna/intern/CMakeLists.txt b/source/blender/makesrna/intern/CMakeLists.txt index 1e8c458539e..58f9cf04913 100644 --- a/source/blender/makesrna/intern/CMakeLists.txt +++ b/source/blender/makesrna/intern/CMakeLists.txt @@ -110,8 +110,10 @@ endif() if(WITH_EXPERIMENTAL_FEATURES) add_definitions(-DWITH_SIMULATION_DATABLOCK) add_definitions(-DWITH_GREASE_PENCIL_V3) + add_definitions(-DWITH_ANIM_BAKLAVA) list(APPEND DEFSRC rna_grease_pencil.cc + rna_animation_id.cc ) endif() @@ -235,6 +237,7 @@ set(INC . .. ../../asset_system + ../../animrig ../../blenfont ../../blenkernel ../../blenlib diff --git a/source/blender/makesrna/intern/makesrna.cc b/source/blender/makesrna/intern/makesrna.cc index 00bdb27d434..6bd09a2d8d7 100644 --- a/source/blender/makesrna/intern/makesrna.cc +++ b/source/blender/makesrna/intern/makesrna.cc @@ -4765,6 +4765,9 @@ static RNAProcessItem PROCESS_ITEMS[] = { {"rna_texture.cc", "rna_texture_api.cc", RNA_def_texture}, {"rna_action.cc", "rna_action_api.cc", RNA_def_action}, {"rna_animation.cc", "rna_animation_api.cc", RNA_def_animation}, +#ifdef WITH_ANIM_BAKLAVA + {"rna_animation_id.cc", nullptr, RNA_def_animation_id}, +#endif {"rna_animviz.cc", nullptr, RNA_def_animviz}, {"rna_armature.cc", "rna_armature_api.cc", RNA_def_armature}, {"rna_attribute.cc", nullptr, RNA_def_attribute}, diff --git a/source/blender/makesrna/intern/rna_ID.cc b/source/blender/makesrna/intern/rna_ID.cc index 4463d41a299..c91ab260908 100644 --- a/source/blender/makesrna/intern/rna_ID.cc +++ b/source/blender/makesrna/intern/rna_ID.cc @@ -123,6 +123,9 @@ static const EnumPropertyItem rna_enum_override_library_property_operation_items const IDFilterEnumPropertyItem rna_enum_id_type_filter_items[] = { /* Datablocks */ {FILTER_ID_AC, "filter_action", ICON_ACTION, "Actions", "Show Action data-blocks"}, +#ifdef WITH_ANIM_BAKLAVA + {FILTER_ID_AN, "filter_animation", ICON_ACTION, "Animations", "Show Animation data-blocks"}, +#endif {FILTER_ID_AR, "filter_armature", ICON_ARMATURE_DATA, @@ -375,6 +378,11 @@ short RNA_type_to_ID_code(const StructRNA *type) if (base_type == &RNA_Action) { return ID_AC; } +# ifdef WITH_ANIM_BAKLAVA + if (base_type == &RNA_Animation) { + return ID_AN; + } +# endif if (base_type == &RNA_Armature) { return ID_AR; } @@ -498,6 +506,9 @@ StructRNA *ID_code_to_RNA_type(short idcode) case ID_AC: return &RNA_Action; case ID_AN: +# ifdef WITH_ANIM_BAKLAVA + return &RNA_Animation; +# endif break; case ID_AR: return &RNA_Armature; diff --git a/source/blender/makesrna/intern/rna_animation_id.cc b/source/blender/makesrna/intern/rna_animation_id.cc new file mode 100644 index 00000000000..853c2a9f9a0 --- /dev/null +++ b/source/blender/makesrna/intern/rna_animation_id.cc @@ -0,0 +1,714 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup RNA + */ + +#include + +#include "DNA_anim_types.h" + +#include "ANIM_animation.hh" + +#include "BLI_utildefines.h" + +#include "BLT_translation.hh" + +#include "MEM_guardedalloc.h" + +#include "RNA_access.hh" +#include "RNA_define.hh" +#include "RNA_enum_types.hh" + +#include "rna_internal.hh" + +#include "WM_api.hh" +#include "WM_types.hh" + +using namespace blender; + +const EnumPropertyItem rna_enum_layer_mix_mode_items[] = { + {int(animrig::Layer::MixMode::Replace), + "REPLACE", + 0, + "Replace", + "Channels in this layer override the same channels from underlying layers"}, + {int(animrig::Layer::MixMode::Offset), + "OFFSET", + 0, + "Offset", + "Channels in this layer are added to underlying layers as sequential operations"}, + {int(animrig::Layer::MixMode::Add), + "ADD", + 0, + "Add", + "Channels in this layer are added to underlying layers on a per-channel basis"}, + {int(animrig::Layer::MixMode::Subtract), + "SUBTRACT", + 0, + "Subtract", + "Channels in this layer are subtracted to underlying layers on a per-channel basis"}, + {int(animrig::Layer::MixMode::Multiply), + "MULTIPLY", + 0, + "Multiply", + "Channels in this layer are multiplied with underlying layers on a per-channel basis"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +const EnumPropertyItem rna_enum_strip_type_items[] = { + {int(animrig::Strip::Type::Keyframe), + "KEYFRAME", + 0, + "Keyframe", + "Strip containing keyframes on F-Curves"}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +#ifdef RNA_RUNTIME + +# include "ANIM_animation.hh" + +# include "DEG_depsgraph.hh" + +# include + +static animrig::Animation &rna_animation(const PointerRNA *ptr) +{ + return reinterpret_cast(ptr->owner_id)->wrap(); +} + +static animrig::Binding &rna_data_binding(const PointerRNA *ptr) +{ + return reinterpret_cast(ptr->data)->wrap(); +} + +static animrig::Layer &rna_data_layer(const PointerRNA *ptr) +{ + return reinterpret_cast(ptr->data)->wrap(); +} + +static animrig::Strip &rna_data_strip(const PointerRNA *ptr) +{ + return reinterpret_cast(ptr->data)->wrap(); +} + +static void rna_Animation_tag_animupdate(Main *, Scene *, PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + DEG_id_tag_update(&anim.id, ID_RECALC_ANIMATION); +} + +static animrig::KeyframeStrip &rna_data_keyframe_strip(const PointerRNA *ptr) +{ + animrig::Strip &strip = reinterpret_cast(ptr->data)->wrap(); + return strip.as(); +} + +static animrig::ChannelBag &rna_data_channelbag(const PointerRNA *ptr) +{ + return reinterpret_cast(ptr->data)->wrap(); +} + +template +static void rna_iterator_array_begin(CollectionPropertyIterator *iter, Span items) +{ + rna_iterator_array_begin(iter, (void *)items.data(), sizeof(T *), items.size(), 0, nullptr); +} + +template +static void rna_iterator_array_begin(CollectionPropertyIterator *iter, MutableSpan items) +{ + rna_iterator_array_begin(iter, (void *)items.data(), sizeof(T *), items.size(), 0, nullptr); +} + +static AnimationBinding *rna_Animation_bindings_new(Animation *anim_id, + bContext *C, + ReportList *reports, + ID *animated_id) +{ + if (animated_id == nullptr) { + BKE_report(reports, + RPT_ERROR, + "A binding without animated ID cannot be created at the moment; if you need it, " + "please file a bug report"); + return nullptr; + } + + animrig::Animation &anim = anim_id->wrap(); + animrig::Binding &binding = anim.binding_add(); + /* TODO: actually set binding->idtype to this ID's type. */ + anim.binding_name_define(binding, animated_id->name); + + WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr); + return &binding; +} + +static void rna_iterator_animation_layers_begin(CollectionPropertyIterator *iter, PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + rna_iterator_array_begin(iter, anim.layers()); +} + +static int rna_iterator_animation_layers_length(PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + return anim.layers().size(); +} + +static AnimationLayer *rna_Animation_layers_new(Animation *dna_animation, + bContext *C, + ReportList *reports, + const char *name) +{ + animrig::Animation &anim = dna_animation->wrap(); + + if (anim.layers().size() >= 1) { + /* Not allowed to have more than one layer, for now. This limitation is in + * place until working with multiple animated IDs is fleshed out better. */ + BKE_report(reports, RPT_ERROR, "An Animation may not have more than one layer"); + return nullptr; + } + + animrig::Layer &layer = anim.layer_add(name); + + WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr); + return &layer; +} + +void rna_Animation_layers_remove(Animation *dna_animation, + bContext *C, + ReportList *reports, + AnimationLayer *dna_layer) +{ + animrig::Animation &anim = dna_animation->wrap(); + animrig::Layer &layer = dna_layer->wrap(); + if (!anim.layer_remove(layer)) { + BKE_report(reports, RPT_ERROR, "This layer does not belong to this animation"); + return; + } + + WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr); + DEG_id_tag_update(&anim.id, ID_RECALC_ANIMATION); +} + +static void rna_iterator_animation_bindings_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + rna_iterator_array_begin(iter, anim.bindings()); +} + +static int rna_iterator_animation_bindings_length(PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + return anim.bindings().size(); +} + +static std::optional rna_AnimationBinding_path(const PointerRNA *ptr) +{ + animrig::Binding &binding = rna_data_binding(ptr); + + char name_esc[sizeof(binding.name) * 2]; + BLI_str_escape(name_esc, binding.name, sizeof(name_esc)); + return fmt::format("bindings[\"{}\"]", name_esc); +} + +static void rna_AnimationBinding_name_set(PointerRNA *ptr, const char *name) +{ + animrig::Animation &anim = rna_animation(ptr); + animrig::Binding &binding = rna_data_binding(ptr); + + anim.binding_name_define(binding, name); +} + +static void rna_AnimationBinding_name_update(Main *bmain, Scene *, PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + animrig::Binding &binding = rna_data_binding(ptr); + + anim.binding_name_propagate(*bmain, binding); +} + +static std::optional rna_AnimationLayer_path(const PointerRNA *ptr) +{ + animrig::Layer &layer = rna_data_layer(ptr); + + char name_esc[sizeof(layer.name) * 2]; + BLI_str_escape(name_esc, layer.name, sizeof(name_esc)); + return fmt::format("layers[\"{}\"]", name_esc); +} + +static void rna_iterator_animationlayer_strips_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + animrig::Layer &layer = rna_data_layer(ptr); + rna_iterator_array_begin(iter, layer.strips()); +} + +static int rna_iterator_animationlayer_strips_length(PointerRNA *ptr) +{ + animrig::Layer &layer = rna_data_layer(ptr); + return layer.strips().size(); +} + +AnimationStrip *rna_AnimationStrips_new(AnimationLayer *dna_layer, + bContext *C, + ReportList *reports, + const int type) +{ + const animrig::Strip::Type strip_type = animrig::Strip::Type(type); + + animrig::Layer &layer = dna_layer->wrap(); + + if (layer.strips().size() >= 1) { + /* Not allowed to have more than one strip, for now. This limitation is in + * place until working with layers is fleshed out better. */ + BKE_report(reports, RPT_ERROR, "A layer may not have more than one strip"); + return nullptr; + } + + animrig::Strip &strip = layer.strip_add(strip_type); + + WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr); + return &strip; +} + +void rna_AnimationStrips_remove(ID *animation_id, + AnimationLayer *dna_layer, + bContext *C, + ReportList *reports, + AnimationStrip *dna_strip) +{ + animrig::Layer &layer = dna_layer->wrap(); + animrig::Strip &strip = dna_strip->wrap(); + if (!layer.strip_remove(strip)) { + BKE_report(reports, RPT_ERROR, "This strip does not belong to this layer"); + return; + } + + WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN, nullptr); + DEG_id_tag_update(animation_id, ID_RECALC_ANIMATION); +} + +static StructRNA *rna_AnimationStrip_refine(PointerRNA *ptr) +{ + animrig::Strip &strip = rna_data_strip(ptr); + switch (strip.type()) { + case animrig::Strip::Type::Keyframe: + return &RNA_KeyframeAnimationStrip; + } + return &RNA_UnknownType; +} + +static std::optional rna_AnimationStrip_path(const PointerRNA *ptr) +{ + animrig::Animation &anim = rna_animation(ptr); + animrig::Strip &strip_to_find = rna_data_strip(ptr); + + for (animrig::Layer *layer : anim.layers()) { + Span strips = layer->strips(); + const int index = strips.first_index_try(&strip_to_find); + if (index < 0) { + continue; + } + + PointerRNA layer_ptr = RNA_pointer_create(&anim.id, &RNA_AnimationLayer, layer); + const std::optional layer_path = rna_AnimationLayer_path(&layer_ptr); + BLI_assert_msg(layer_path, "Every animation layer should have a valid RNA path."); + const std::string strip_path = fmt::format("{}.strips[{}]", *layer_path, index); + return strip_path; + } + + return std::nullopt; +} + +static void rna_iterator_keyframestrip_channelbags_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + animrig::KeyframeStrip &key_strip = rna_data_keyframe_strip(ptr); + rna_iterator_array_begin(iter, key_strip.channelbags()); +} + +static int rna_iterator_keyframestrip_channelbags_length(PointerRNA *ptr) +{ + animrig::KeyframeStrip &key_strip = rna_data_keyframe_strip(ptr); + return key_strip.channelbags().size(); +} + +static void rna_iterator_ChannelBag_fcurves_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + animrig::ChannelBag &bag = rna_data_channelbag(ptr); + rna_iterator_array_begin(iter, bag.fcurves()); +} + +static int rna_iterator_ChannelBag_fcurves_length(PointerRNA *ptr) +{ + animrig::ChannelBag &bag = rna_data_channelbag(ptr); + return bag.fcurves().size(); +} + +static AnimationChannelBag *rna_KeyframeAnimationStrip_channels( + KeyframeAnimationStrip *self, const animrig::binding_handle_t binding_handle) +{ + animrig::KeyframeStrip &key_strip = self->wrap(); + return key_strip.channelbag_for_binding(binding_handle); +} + +#else + +static void rna_def_animation_bindings(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + + FunctionRNA *func; + PropertyRNA *parm; + + RNA_def_property_srna(cprop, "AnimationBindings"); + srna = RNA_def_struct(brna, "AnimationBindings", nullptr); + RNA_def_struct_sdna(srna, "Animation"); + RNA_def_struct_ui_text(srna, "Animation Bindings", "Collection of animation bindings"); + + /* Animation.bindings.new(...) */ + func = RNA_def_function(srna, "new", "rna_Animation_bindings_new"); + RNA_def_function_ui_description(func, "Add a binding to the animation"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + parm = RNA_def_pointer( + func, "animated_id", "ID", "Data-Block", "Data-block that will be animated by this binding"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_pointer( + func, "binding", "AnimationBinding", "", "Newly created animation binding"); + RNA_def_function_return(func, parm); +} + +static void rna_def_animation_layers(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + + FunctionRNA *func; + PropertyRNA *parm; + + RNA_def_property_srna(cprop, "AnimationLayers"); + srna = RNA_def_struct(brna, "AnimationLayers", nullptr); + RNA_def_struct_sdna(srna, "Animation"); + RNA_def_struct_ui_text(srna, "Animation Layers", "Collection of animation layers"); + + /* Animation.layers.new(...) */ + func = RNA_def_function(srna, "new", "rna_Animation_layers_new"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + RNA_def_function_ui_description( + func, + "Add a layer to the Animation. Currently an Animation can only have at most one layer"); + parm = RNA_def_string(func, + "name", + nullptr, + sizeof(AnimationLayer::name) - 1, + "Name", + "Name of the layer, will be made unique within the Animation data-block"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_pointer(func, "layer", "AnimationLayer", "", "Newly created animation layer"); + RNA_def_function_return(func, parm); + + /* Animation.layers.remove(layer) */ + func = RNA_def_function(srna, "remove", "rna_Animation_layers_remove"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + RNA_def_function_ui_description(func, "Remove the layer from the animation"); + parm = RNA_def_pointer( + func, "anim_layer", "AnimationLayer", "Animation Layer", "The layer to remove"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); +} + +static void rna_def_animation(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "Animation", "ID"); + RNA_def_struct_sdna(srna, "Animation"); + RNA_def_struct_ui_text(srna, "Animation", "A collection of animation layers"); + RNA_def_struct_ui_icon(srna, ICON_ACTION); + + prop = RNA_def_property(srna, "last_binding_handle", PROP_INT, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + + /* Collection properties .*/ + prop = RNA_def_property(srna, "bindings", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "AnimationBinding"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_animation_bindings_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_animation_bindings_length", + nullptr, + nullptr, + nullptr); + RNA_def_property_ui_text(prop, "Bindings", "The list of bindings in this animation data-block"); + rna_def_animation_bindings(brna, prop); + + prop = RNA_def_property(srna, "layers", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "AnimationLayer"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_animation_layers_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_animation_layers_length", + nullptr, + nullptr, + nullptr); + RNA_def_property_ui_text(prop, "Layers", "The list of layers that make up this Animation"); + rna_def_animation_layers(brna, prop); +} + +static void rna_def_animation_binding(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "AnimationBinding", nullptr); + RNA_def_struct_path_func(srna, "rna_AnimationBinding_path"); + RNA_def_struct_ui_text( + srna, + "Animation Binding", + "Identifier for a set of channels in this Animation, that can be used by a data-block " + "to specify what it gets animated by"); + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_struct_name_property(srna, prop); + RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_AnimationBinding_name_set"); + RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_AnimationBinding_name_update"); + RNA_def_struct_ui_text( + srna, + "Binding Name", + "Used when connecting an Animation to a data-block, to find the correct binding handle"); + + prop = RNA_def_property(srna, "handle", PROP_INT, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_struct_ui_text(srna, + "Binding Handle", + "Number specific to this Binding, unique within the Animation data-block" + "This is used, for example, on a KeyframeAnimationStrip to look up the " + "AnimationChannelBag for this Binding"); +} + +static void rna_def_animationlayer_strips(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + + FunctionRNA *func; + PropertyRNA *parm; + + RNA_def_property_srna(cprop, "AnimationStrips"); + srna = RNA_def_struct(brna, "AnimationStrips", nullptr); + RNA_def_struct_sdna(srna, "AnimationLayer"); + RNA_def_struct_ui_text(srna, "Animation Strips", "Collection of animation strips"); + + /* Layer.strips.new(type='...') */ + func = RNA_def_function(srna, "new", "rna_AnimationStrips_new"); + RNA_def_function_ui_description(func, + "Add a new strip to the layer. Currently a layer can only have " + "one strip, with infinite boundaries"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + parm = RNA_def_enum(func, + "type", + rna_enum_strip_type_items, + int(animrig::Strip::Type::Keyframe), + "Type", + "The type of strip to create"); + /* Return value. */ + parm = RNA_def_pointer(func, "strip", "AnimationStrip", "", "Newly created animation strip"); + RNA_def_function_return(func, parm); + + /* Layer.strips.remove(strip) */ + func = RNA_def_function(srna, "remove", "rna_AnimationStrips_remove"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + RNA_def_function_ui_description(func, "Remove the strip from the animation layer"); + parm = RNA_def_pointer( + func, "anim_strip", "AnimationStrip", "Animation Strip", "The strip to remove"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); +} + +static void rna_def_animation_layer(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "AnimationLayer", nullptr); + RNA_def_struct_ui_text(srna, "Animation Layer", ""); + RNA_def_struct_path_func(srna, "rna_AnimationLayer_path"); + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_struct_name_property(srna, prop); + + prop = RNA_def_property(srna, "influence", PROP_FLOAT, PROP_FACTOR); + RNA_def_property_range(prop, 0.0f, 1.0f); + RNA_def_property_ui_text( + prop, "Influence", "How much of this layer is used when blending into the lower layers"); + RNA_def_property_ui_range(prop, 0.0, 1.0, 3, 2); + RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY); + RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_Animation_tag_animupdate"); + + prop = RNA_def_property(srna, "mix_mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "layer_mix_mode"); + RNA_def_property_ui_text( + prop, "Mix Mode", "How animation of this layer is blended into the lower layers"); + RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY); + RNA_def_property_enum_items(prop, rna_enum_layer_mix_mode_items); + RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN, "rna_Animation_tag_animupdate"); + + /* Collection properties .*/ + prop = RNA_def_property(srna, "strips", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "AnimationStrip"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_animationlayer_strips_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_animationlayer_strips_length", + nullptr, + nullptr, + nullptr); + RNA_def_property_ui_text(prop, "Strips", "The list of strips that are on this animation layer"); + + rna_def_animationlayer_strips(brna, prop); +} + +static void rna_def_keyframestrip_channelbags(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + + RNA_def_property_srna(cprop, "AnimationChannelBags"); + srna = RNA_def_struct(brna, "AnimationChannelBags", nullptr); + RNA_def_struct_sdna(srna, "KeyframeAnimationStrip"); + RNA_def_struct_ui_text( + srna, + "Animation Channels for Bindings", + "For each animation binding, a list of animation channels that are meant for that binding"); +} + +static void rna_def_animation_keyframe_strip(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "KeyframeAnimationStrip", "AnimationStrip"); + RNA_def_struct_ui_text( + srna, "Keyframe Animation Strip", "Strip with a set of F-Curves for each animation binding"); + + prop = RNA_def_property(srna, "channelbags", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "AnimationChannelBag"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_keyframestrip_channelbags_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_keyframestrip_channelbags_length", + nullptr, + nullptr, + nullptr); + rna_def_keyframestrip_channelbags(brna, prop); + + { + FunctionRNA *func; + PropertyRNA *parm; + + /* KeyframeStrip.channels(...). */ + func = RNA_def_function(srna, "channels", "rna_KeyframeAnimationStrip_channels"); + RNA_def_function_ui_description(func, "Find the AnimationChannelBag for a specific Binding"); + parm = RNA_def_int(func, + "binding_handle", + 0, + 0, + INT_MAX, + "Binding Handle", + "Number that identifies a specific animation binding", + 0, + INT_MAX); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_pointer(func, "channels", "AnimationChannelBag", "Channels", ""); + RNA_def_function_return(func, parm); + } +} + +static void rna_def_animation_strip(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "AnimationStrip", nullptr); + RNA_def_struct_ui_text(srna, "Animation Strip", ""); + RNA_def_struct_path_func(srna, "rna_AnimationStrip_path"); + RNA_def_struct_refine_func(srna, "rna_AnimationStrip_refine"); + + static const EnumPropertyItem prop_type_items[] = { + {int(animrig::Strip::Type::Keyframe), + "KEYFRAME", + 0, + "Keyframe", + "Strip with a set of F-Curves for each animation binding"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + prop = RNA_def_property(srna, "type", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "strip_type"); + RNA_def_property_enum_items(prop, prop_type_items); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + + /* Define Strip subclasses. */ + rna_def_animation_keyframe_strip(brna); +} + +static void rna_def_channelbag_for_binding_fcurves(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + + RNA_def_property_srna(cprop, "AnimationChannelBagFCurves"); + srna = RNA_def_struct(brna, "AnimationChannelBagFCurves", nullptr); + RNA_def_struct_sdna(srna, "bAnimationChannelBag"); + RNA_def_struct_ui_text( + srna, "F-Curves", "Collection of F-Curves for a specific animation binding"); +} + +static void rna_def_animation_channelbag(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "AnimationChannelBag", nullptr); + RNA_def_struct_ui_text( + srna, + "Animation Channel Bag", + "Collection of animation channels, typically associated with an animation binding"); + + prop = RNA_def_property(srna, "binding_handle", PROP_INT, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + + prop = RNA_def_property(srna, "fcurves", PROP_COLLECTION, PROP_NONE); + RNA_def_property_collection_funcs(prop, + "rna_iterator_ChannelBag_fcurves_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_ChannelBag_fcurves_length", + nullptr, + nullptr, + nullptr); + RNA_def_property_struct_type(prop, "FCurve"); + RNA_def_property_ui_text(prop, "F-Curves", "The individual F-Curves that animate the binding"); + rna_def_channelbag_for_binding_fcurves(brna, prop); +} + +void RNA_def_animation_id(BlenderRNA *brna) +{ + rna_def_animation(brna); + rna_def_animation_binding(brna); + rna_def_animation_layer(brna); + rna_def_animation_strip(brna); + rna_def_animation_channelbag(brna); +} + +#endif diff --git a/source/blender/makesrna/intern/rna_internal.hh b/source/blender/makesrna/intern/rna_internal.hh index b1482cf4ad2..34ffa800048 100644 --- a/source/blender/makesrna/intern/rna_internal.hh +++ b/source/blender/makesrna/intern/rna_internal.hh @@ -139,6 +139,9 @@ extern BlenderRNA BLENDER_RNA; void RNA_def_ID(BlenderRNA *brna); void RNA_def_action(BlenderRNA *brna); void RNA_def_animation(BlenderRNA *brna); +#ifdef WITH_ANIM_BAKLAVA +void RNA_def_animation_id(BlenderRNA *brna); +#endif void RNA_def_animviz(BlenderRNA *brna); void RNA_def_armature(BlenderRNA *brna); void RNA_def_attribute(BlenderRNA *brna); @@ -486,6 +489,9 @@ void RNA_def_main_speakers(BlenderRNA *brna, PropertyRNA *cprop); void RNA_def_main_sounds(BlenderRNA *brna, PropertyRNA *cprop); void RNA_def_main_armatures(BlenderRNA *brna, PropertyRNA *cprop); void RNA_def_main_actions(BlenderRNA *brna, PropertyRNA *cprop); +#ifdef WITH_ANIM_BAKLAVA +void RNA_def_main_animations(BlenderRNA *brna, PropertyRNA *cprop); +#endif void RNA_def_main_particles(BlenderRNA *brna, PropertyRNA *cprop); void RNA_def_main_palettes(BlenderRNA *brna, PropertyRNA *cprop); void RNA_def_main_gpencil_legacy(BlenderRNA *brna, PropertyRNA *cprop); diff --git a/source/blender/makesrna/intern/rna_main.cc b/source/blender/makesrna/intern/rna_main.cc index e02bc64bab1..d7e5d17375a 100644 --- a/source/blender/makesrna/intern/rna_main.cc +++ b/source/blender/makesrna/intern/rna_main.cc @@ -90,6 +90,9 @@ static void rna_Main_filepath_set(PointerRNA *ptr, const char *value) } RNA_MAIN_LISTBASE_FUNCS_DEF(actions) +# ifdef WITH_ANIM_BAKLAVA +RNA_MAIN_LISTBASE_FUNCS_DEF(animations) +# endif RNA_MAIN_LISTBASE_FUNCS_DEF(armatures) RNA_MAIN_LISTBASE_FUNCS_DEF(brushes) RNA_MAIN_LISTBASE_FUNCS_DEF(cachefiles) @@ -319,6 +322,14 @@ void RNA_def_main(BlenderRNA *brna) "Actions", "Action data-blocks", RNA_def_main_actions}, +# ifdef WITH_ANIM_BAKLAVA + {"animations", + "Animation", + "rna_Main_animations_begin", + "animations", + "Animation data-blocks", + RNA_def_main_animations}, +# endif {"particles", "ParticleSettings", "rna_Main_particles_begin", diff --git a/source/blender/makesrna/intern/rna_main_api.cc b/source/blender/makesrna/intern/rna_main_api.cc index acb1d52ff92..4cf07e94c2e 100644 --- a/source/blender/makesrna/intern/rna_main_api.cc +++ b/source/blender/makesrna/intern/rna_main_api.cc @@ -26,6 +26,7 @@ #ifdef RNA_RUNTIME # include "BKE_action.h" +# include "BKE_animation.hh" # include "BKE_armature.hh" # include "BKE_brush.hh" # include "BKE_camera.h" @@ -65,6 +66,7 @@ # include "DEG_depsgraph_build.hh" # include "DEG_depsgraph_query.hh" +# include "DNA_anim_types.h" # include "DNA_armature_types.h" # include "DNA_brush_types.h" # include "DNA_camera_types.h" @@ -635,6 +637,22 @@ static bAction *rna_Main_actions_new(Main *bmain, const char *name) return act; } +# ifdef WITH_ANIM_BAKLAVA +static Animation *rna_Main_animations_new(Main *bmain, const char *name) +{ + char safe_name[MAX_ID_NAME - 2]; + rna_idname_validate(name, safe_name); + + Animation *anim = BKE_animation_add(bmain, safe_name); + id_fake_user_clear(&anim->id); + id_us_min(&anim->id); + + WM_main_add_notifier(NC_ID | NA_ADDED, nullptr); + + return anim; +} +# endif + static ParticleSettings *rna_Main_particles_new(Main *bmain, const char *name) { char safe_name[MAX_ID_NAME - 2]; @@ -821,6 +839,9 @@ RNA_MAIN_ID_TAG_FUNCS_DEF(speakers, speakers, ID_SPK) RNA_MAIN_ID_TAG_FUNCS_DEF(sounds, sounds, ID_SO) RNA_MAIN_ID_TAG_FUNCS_DEF(armatures, armatures, ID_AR) RNA_MAIN_ID_TAG_FUNCS_DEF(actions, actions, ID_AC) +# ifdef WITH_ANIM_BAKLAVA +RNA_MAIN_ID_TAG_FUNCS_DEF(animations, animations, ID_AN) +# endif RNA_MAIN_ID_TAG_FUNCS_DEF(particles, particles, ID_PA) RNA_MAIN_ID_TAG_FUNCS_DEF(palettes, palettes, ID_PAL) RNA_MAIN_ID_TAG_FUNCS_DEF(gpencils, gpencils, ID_GD_LEGACY) @@ -1881,6 +1902,49 @@ void RNA_def_main_actions(BlenderRNA *brna, PropertyRNA *cprop) parm = RNA_def_boolean(func, "value", false, "Value", ""); RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); } +# ifdef WITH_ANIM_BAKLAVA +void RNA_def_main_animations(BlenderRNA *brna, PropertyRNA *cprop) +{ + StructRNA *srna; + FunctionRNA *func; + PropertyRNA *parm; + + RNA_def_property_srna(cprop, "BlendDataAnimations"); + srna = RNA_def_struct(brna, "BlendDataAnimations", nullptr); + RNA_def_struct_sdna(srna, "Main"); + RNA_def_struct_ui_text(srna, "Main Animations", "Collection of animation data-blocks"); + + func = RNA_def_function(srna, "new", "rna_Main_animations_new"); + RNA_def_function_ui_description(func, "Add a new animation data-block to the main database"); + parm = RNA_def_string(func, "name", "Animation", 0, "", "Name for the new data-block"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + /* return type */ + parm = RNA_def_pointer(func, "animation", "Animation", "", "New animation data-block"); + RNA_def_function_return(func, parm); + + func = RNA_def_function(srna, "remove", "rna_Main_ID_remove"); + RNA_def_function_flag(func, FUNC_USE_REPORTS); + RNA_def_function_ui_description(func, + "Remove an animation data-block from the current blendfile"); + parm = RNA_def_pointer(func, "animation", "Animation", "", "Animation to remove"); + RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED | PARM_RNAPTR); + RNA_def_parameter_clear_flags(parm, PROP_THICK_WRAP, ParameterFlag(0)); + RNA_def_boolean( + func, "do_unlink", true, "", "Unlink all usages of this animation before deleting it"); + RNA_def_boolean(func, + "do_id_user", + true, + "", + "Decrement user counter of all datablocks used by this animation"); + RNA_def_boolean( + func, "do_ui_user", true, "", "Make sure interface does not reference this animation"); + + /* Defined via RNA_MAIN_LISTBASE_FUNCS_DEF. */ + func = RNA_def_function(srna, "tag", "rna_Main_animations_tag"); + parm = RNA_def_boolean(func, "value", false, "Value", ""); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); +} +# endif void RNA_def_main_particles(BlenderRNA *brna, PropertyRNA *cprop) { StructRNA *srna; diff --git a/source/blender/makesrna/intern/rna_space.cc b/source/blender/makesrna/intern/rna_space.cc index c9707e96a44..e6f784c2e40 100644 --- a/source/blender/makesrna/intern/rna_space.cc +++ b/source/blender/makesrna/intern/rna_space.cc @@ -3441,7 +3441,11 @@ static const EnumPropertyItem dt_uv_items[] = { static IDFilterEnumPropertyItem rna_enum_space_file_id_filter_categories[] = { /* Categories */ {FILTER_ID_SCE, "category_scene", ICON_SCENE_DATA, "Scenes", "Show scenes"}, - {FILTER_ID_AC, "category_animation", ICON_ANIM_DATA, "Animations", "Show animation data"}, + {FILTER_ID_AC | FILTER_ID_AN, + "category_animation", + ICON_ANIM_DATA, + "Animations", + "Show animation data"}, {FILTER_ID_OB | FILTER_ID_GR, "category_object", ICON_OUTLINER_COLLECTION, diff --git a/source/blender/makesrna/intern/rna_userdef.cc b/source/blender/makesrna/intern/rna_userdef.cc index ebea4b45dab..0d23d758c20 100644 --- a/source/blender/makesrna/intern/rna_userdef.cc +++ b/source/blender/makesrna/intern/rna_userdef.cc @@ -7218,6 +7218,13 @@ static void rna_def_userdef_experimental(BlenderRNA *brna) RNA_def_property_ui_text( prop, "Extensions Development Utilities", "Developer support utilities for extensions"); RNA_def_property_update(prop, 0, "rna_userdef_update"); + + prop = RNA_def_property(srna, "use_animation_baklava", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "use_animation_baklava", 1); + RNA_def_property_ui_text(prop, + "Animation: Project Baklava", + "Enable the new multi-data-block, layered animation system"); + RNA_def_property_update(prop, 0, "rna_userdef_update"); } static void rna_def_userdef_addon_collection(BlenderRNA *brna, PropertyRNA *cprop) diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index e1ed92ab8da..a7df8b9af18 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -386,6 +386,14 @@ add_blender_test( --testdir "${TEST_SRC_DIR}/animation" ) +if(WITH_EXPERIMENTAL_FEATURES) + # Only run with Project Baklava enabled. + add_blender_test( + bl_animation_id + --python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_id.py + ) +endif() + add_blender_test( bl_animation_keyframing --python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_keyframing.py diff --git a/tests/python/bl_animation_id.py b/tests/python/bl_animation_id.py new file mode 100644 index 00000000000..4919a15f472 --- /dev/null +++ b/tests/python/bl_animation_id.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2020-2023 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import unittest +import sys + +import bpy + +""" +blender -b --factory-startup --python tests/python/bl_animation_id.py +""" + + +class LimitationsTest(unittest.TestCase): + """Test artificial limitations for the Animation data-block. + + Certain limitations are in place to keep development & testing focused. + """ + + def setUp(self): + anims = bpy.data.animations + while anims: + anims.remove(anims[0]) + + def test_initial_layers(self): + """Test that upon creation an Animation has no layers/strips.""" + anim = bpy.data.animations.new('TestAnim') + self.assertEqual([], anim.layers[:]) + + def test_limited_layers_strips(self): + """Test that there can only be one layer with one strip.""" + + anim = bpy.data.animations.new('TestAnim') + layer = anim.layers.new(name="Layer") + self.assertEqual([], layer.strips[:]) + strip = layer.strips.new(type='KEYFRAME') + + # Adding a 2nd layer should be forbidden. + with self.assertRaises(RuntimeError): + anim.layers.new(name="Forbidden Layer") + self.assertEqual([layer], anim.layers[:]) + + # Adding a 2nd strip should be forbidden. + with self.assertRaises(RuntimeError): + layer.strips.new(type='KEYFRAME') + self.assertEqual([strip], layer.strips[:]) + + def test_limited_strip_api(self): + """Test that strips have no frame start/end/offset properties.""" + + anim = bpy.data.animations.new('TestAnim') + layer = anim.layers.new(name="Layer") + strip = layer.strips.new(type='KEYFRAME') + + self.assertFalse(hasattr(strip, 'frame_start')) + self.assertFalse(hasattr(strip, 'frame_end')) + self.assertFalse(hasattr(strip, 'frame_offset')) + + +class DataPathTest(unittest.TestCase): + def setUp(self): + anims = bpy.data.animations + while anims: + anims.remove(anims[0]) + + def test_repr(self): + anim = bpy.data.animations.new('TestAnim') + + layer = anim.layers.new(name="Layer") + self.assertEqual("bpy.data.animations['TestAnim'].layers[\"Layer\"]", repr(layer)) + + strip = layer.strips.new(type='KEYFRAME') + self.assertEqual("bpy.data.animations['TestAnim'].layers[\"Layer\"].strips[0]", repr(strip)) + + +def main(): + global args + import argparse + + argv = [sys.argv[0]] + if '--' in sys.argv: + argv += sys.argv[sys.argv.index('--') + 1:] + + parser = argparse.ArgumentParser() + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main()