From 1d2cea1e3e381a83c0c77e74fd8518257de91541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 4 Jul 2024 14:44:19 +0200 Subject: [PATCH] Show Action Slots (Bindings) in the Action editor In the Action mode of the Dope Sheet, show the Bindings, optionally showing all Bindings in that Action. This also works when different data-block types are animated from the same Action. Bindings are selectable in the Action editor with box/range select. Since there is no use for an 'active Binding' yet, this has not been implemented, and as a result, clicking on a Binding does nothing. Note on 'Binding' vs 'Slot': this code change was created before the decision to change the name from 'Bindings' to 'Slots'. To avoid confusion, and to keep this PR in sync with the code changes, it still uses the term 'binding'. Renames of 'Binding' to 'Slot' in the code will happen after this PR lands. Pull Request: https://projects.blender.org/blender/blender/pulls/122672 --- scripts/startup/bl_ui/space_dopesheet.py | 3 + source/blender/animrig/ANIM_action.hh | 35 +++ source/blender/animrig/intern/action.cc | 41 +++ .../blender/editors/animation/CMakeLists.txt | 1 + .../animation/anim_channels_defines.cc | 151 ++++++++-- .../editors/animation/anim_channels_edit.cc | 60 +++- source/blender/editors/animation/anim_deps.cc | 1 + .../blender/editors/animation/anim_filter.cc | 275 +++++++++++++---- .../editors/animation/anim_filter_test.cc | 277 ++++++++++++++++++ .../editors/animation/anim_ipo_utils.cc | 81 +++++ .../editors/animation/keyframes_draw.cc | 35 +++ .../editors/animation/keyframes_edit.cc | 48 +-- .../editors/animation/keyframes_keylist.cc | 19 +- source/blender/editors/include/ED_anim_api.hh | 32 +- .../editors/include/ED_keyframes_draw.hh | 8 + .../editors/include/ED_keyframes_keylist.hh | 13 + .../editors/space_action/action_draw.cc | 13 + .../editors/space_action/action_select.cc | 10 + .../blender/editors/space_nla/nla_buttons.cc | 1 + source/blender/editors/space_nla/nla_draw.cc | 1 + .../blender/editors/space_nla/nla_tracks.cc | 3 +- .../transform/transform_convert_action.cc | 1 + source/blender/makesdna/DNA_action_types.h | 18 +- source/blender/makesrna/intern/rna_action.cc | 6 + 24 files changed, 1022 insertions(+), 111 deletions(-) create mode 100644 source/blender/editors/animation/anim_filter_test.cc diff --git a/scripts/startup/bl_ui/space_dopesheet.py b/scripts/startup/bl_ui/space_dopesheet.py index 08b3242e0e2..3113c49cf09 100644 --- a/scripts/startup/bl_ui/space_dopesheet.py +++ b/scripts/startup/bl_ui/space_dopesheet.py @@ -34,8 +34,11 @@ from rna_prop_ui import PropertyPanel def dopesheet_filter(layout, context): dopesheet = context.space_data.dopesheet is_nla = context.area.type == 'NLA_EDITOR' + is_action_editor = not is_nla and context.space_data.mode == 'ACTION' row = layout.row(align=True) + if is_action_editor and context.preferences.experimental.use_animation_baklava: + row.prop(dopesheet, "show_all_bindings", text="") row.prop(dopesheet, "show_only_selected", text="") row.prop(dopesheet, "show_hidden", text="") diff --git a/source/blender/animrig/ANIM_action.hh b/source/blender/animrig/ANIM_action.hh index 5b513afa1b8..e1a2115c8ce 100644 --- a/source/blender/animrig/ANIM_action.hh +++ b/source/blender/animrig/ANIM_action.hh @@ -131,6 +131,14 @@ class Action : public ::bAction { const Binding *binding(int64_t index) const; Binding *binding(int64_t index); + /** + * Return the Binding with the given handle. + * + * \param handle can be `Binding::unassigned`, in which case `nullptr` is returned. + * + * \return `nullptr` when the binding cannot be found, so either the handle was + * `Binding::unassigned` or some value that does not match any Binding in this Action. + */ Binding *binding_for_handle(binding_handle_t handle); const Binding *binding_for_handle(binding_handle_t handle) const; @@ -504,6 +512,21 @@ class Binding : public ::ActionBinding { /** Return whether this Binding has an `idtype` set. */ bool has_idtype() const; + /* Flags access. */ + enum class Flags : uint8_t { + /** Expanded/collapsed in animation editors. */ + Expanded = (1 << 0), + /** Selected in animation editors. */ + Selected = (1 << 1), + /* When adding/removing a flag, also update the ENUM_OPERATORS() invocation, + * all the way below the Binding class. */ + }; + Flags flags() const; + bool is_expanded() const; + void set_expanded(bool expanded); + bool is_selected() const; + void set_selected(bool selected); + /** Return the set of IDs that are animated by this Binding. */ Span users(Main &bmain) const; @@ -559,6 +582,7 @@ class Binding : public ::ActionBinding { }; static_assert(sizeof(Binding) == sizeof(::ActionBinding), "DNA struct and its C++ wrapper must have the same size"); +ENUM_OPERATORS(Binding::Flags, Binding::Flags::Selected); /** * KeyframeStrips effectively contain a bag of F-Curves for each Binding. @@ -713,6 +737,17 @@ Action *get_animation(ID &animated_id); */ std::optional> get_action_binding_pair(ID &animated_id); +/** + * Get the Action and the Binding that animate this ID. + * + * \return One of two options: + * - pair when an Action and a Binding are assigned. In other + * words, when this ID is actually animated by this Action+Binding pair. + * - nullopt: when this ID is not animated. This can have several causes: not + * an animatable type, no Action assigned, or no Binding assigned. + */ +std::optional> get_action_binding_pair(ID &animated_id); + /** * Return the F-Curves for this specific binding handle. * diff --git a/source/blender/animrig/intern/action.cc b/source/blender/animrig/intern/action.cc index ab364d8fe30..341d229bd13 100644 --- a/source/blender/animrig/intern/action.cc +++ b/source/blender/animrig/intern/action.cc @@ -250,6 +250,10 @@ Binding *Action::binding_for_handle(const binding_handle_t handle) const Binding *Action::binding_for_handle(const binding_handle_t handle) const { + if (handle == Binding::unassigned) { + return nullptr; + } + /* TODO: implement hash-map lookup. */ for (const Binding *binding : bindings()) { if (binding->handle == handle) { @@ -352,6 +356,11 @@ Binding &Action::binding_allocate() this->last_binding_handle++; BLI_assert_msg(this->last_binding_handle > 0, "Animation Binding handle overflow"); binding.handle = this->last_binding_handle; + + /* Set the default flags. These cannot be set via the 'DNA defaults' system, + * as that would require knowing which bit corresponds with which flag. That's + * only known to the C++ wrapper code. */ + binding.set_expanded(true); return binding; } @@ -668,6 +677,38 @@ bool Binding::has_idtype() const return this->idtype != 0; } +Binding::Flags Binding::flags() const +{ + return static_cast(this->binding_flags); +} +bool Binding::is_expanded() const +{ + return this->binding_flags & uint8_t(Flags::Expanded); +} +void Binding::set_expanded(const bool expanded) +{ + if (expanded) { + this->binding_flags |= uint8_t(Flags::Expanded); + } + else { + this->binding_flags &= ~(uint8_t(Flags::Expanded)); + } +} + +bool Binding::is_selected() const +{ + return this->binding_flags & uint8_t(Flags::Selected); +} +void Binding::set_selected(const bool selected) +{ + if (selected) { + this->binding_flags |= uint8_t(Flags::Selected); + } + else { + this->binding_flags &= ~(uint8_t(Flags::Selected)); + } +} + Span Binding::users(Main &bmain) const { if (bmain.is_action_binding_to_id_map_dirty) { diff --git a/source/blender/editors/animation/CMakeLists.txt b/source/blender/editors/animation/CMakeLists.txt index 120cb2e8362..b580426b778 100644 --- a/source/blender/editors/animation/CMakeLists.txt +++ b/source/blender/editors/animation/CMakeLists.txt @@ -66,6 +66,7 @@ add_dependencies(bf_editor_animation bf_rna) if(WITH_GTESTS) set(TEST_SRC + anim_filter_test.cc keyframes_keylist_test.cc ) set(TEST_INC diff --git a/source/blender/editors/animation/anim_channels_defines.cc b/source/blender/editors/animation/anim_channels_defines.cc index b9ac4886215..d2289493934 100644 --- a/source/blender/editors/animation/anim_channels_defines.cc +++ b/source/blender/editors/animation/anim_channels_defines.cc @@ -232,20 +232,17 @@ static short acf_generic_indentation_2(bAnimContext *ac, bAnimListElem *ale) /* indentation which varies with the grouping status */ static short acf_generic_indentation_flexible(bAnimContext * /*ac*/, bAnimListElem *ale) { - short indent = 0; - - /* grouped F-Curves need extra level of indentation */ - if (ale->type == ANIMTYPE_FCURVE) { - FCurve *fcu = (FCurve *)ale->data; - - /* TODO: we need some way of specifying that the indentation color should be one less. */ - if (fcu->grp) { - indent++; - } + if (ale->type != ANIMTYPE_FCURVE) { + return 0; } - /* no indentation */ - return indent; + /* Grouped F-Curves need extra level of indentation. */ + const FCurve *fcu = static_cast(ale->data); + if (fcu->grp) { + return 1; + } + + return 0; } /* basic offset for channels derived from indentation */ @@ -297,10 +294,9 @@ static short acf_generic_group_offset(bAnimContext *ac, bAnimListElem *ale) /* materials and particles animdata */ else if (ELEM(GS(ale->id->name), ID_MA, ID_PA)) { offset += short(0.7f * U.widget_unit); - - /* If not in Action Editor mode, action-groups (and their children) - * must carry some offset too. */ } + /* If not in Action Editor mode, action-groups (and their children) + * must carry some offset too. */ else if (ac->datatype != ANIMCONT_ACTION) { offset += short(0.7f * U.widget_unit); } @@ -975,7 +971,35 @@ static bAnimChannelType ACF_GROUP = { /* name for fcurve entries */ static void acf_fcurve_name(bAnimListElem *ale, char *name) { - getname_anim_fcurve(name, ale->id, static_cast(ale->data)); + using namespace blender::animrig; + + FCurve *fcurve = static_cast(ale->data); + + if (ale->fcurve_owner_id && GS(ale->fcurve_owner_id->name) == ID_AC && + ale->binding_handle != Binding::unassigned) + { + /* F-Curve comes from a layered Action. This means that we cannot trust `ale->id` or + * `ale->adt`, because in the Action Editor those are set to whatever object has this Action + * assigned. This F-Curve may be for a different binding, though, and thus might be animating a + * entirely different ID type. */ + const Action &action = reinterpret_cast(ale->fcurve_owner_id)->wrap(); + const Binding *binding = action.binding_for_handle(ale->binding_handle); + if (!binding) { + /* Defer to the default F-Curve resolution, but without the animated ID + * pointer, as it's likely to be wrong anyway. */ + getname_anim_fcurve(name, nullptr, fcurve); + return; + } + + BLI_assert(ale->bmain); + const std::string fcurve_name = getname_anim_fcurve_bound(*ale->bmain, *binding, *fcurve); + const size_t num_chars_copied = fcurve_name.copy(name, std::string::npos); + name[num_chars_copied] = '\0'; + + return; + } + + getname_anim_fcurve(name, ale->id, fcurve); } /* "name" property for fcurve entries */ @@ -1335,7 +1359,94 @@ static bAnimChannelType ACF_FILLANIM = { /*setting_ptr*/ acf_fillanim_setting_ptr, }; -#endif +static void acf_action_binding_name(bAnimListElem *ale, char *r_name) +{ + const animrig::Binding *binding = static_cast(ale->data); + if (!binding) { + /* Trying to getting the binding's name without a binding is a bug. */ + BLI_assert_unreachable(); + BLI_strncpy(r_name, "-nil-", ANIM_CHAN_NAME_SIZE); + return; + } + + BLI_strncpy(r_name, binding->name_without_prefix().c_str(), ANIM_CHAN_NAME_SIZE); +} +static bool acf_action_binding_name_prop(bAnimListElem *ale, + PointerRNA *r_ptr, + PropertyRNA **r_prop) +{ + animrig::Binding *binding = static_cast(ale->data); + BLI_assert(GS(ale->fcurve_owner_id->name) == ID_AC); + + *r_ptr = RNA_pointer_create(ale->fcurve_owner_id, &RNA_ActionBinding, binding); + *r_prop = RNA_struct_find_property(r_ptr, "name_display"); + + return (*r_prop != nullptr); +} + +static int acf_action_binding_icon(bAnimListElem *ale) +{ + animrig::Binding *binding = static_cast(ale->data); + return UI_icon_from_idcode(binding->idtype); +} + +static bool acf_action_binding_setting_valid(bAnimContext * /*ac*/, + bAnimListElem * /*ale*/, + const eAnimChannel_Settings setting) +{ + switch (setting) { + case ACHANNEL_SETTING_SELECT: + case ACHANNEL_SETTING_EXPAND: + return true; + + default: + return false; + } +} + +static int acf_action_binding_setting_flag(bAnimContext * /*ac*/, + eAnimChannel_Settings setting, + bool *r_neg) +{ + *r_neg = false; + + switch (setting) { + case ACHANNEL_SETTING_SELECT: + return int(animrig::Binding::Flags::Selected); + case ACHANNEL_SETTING_EXPAND: + return int(animrig::Binding::Flags::Expanded); + default: + return 0; + } +} +static void *acf_action_binding_setting_ptr(bAnimListElem *ale, + eAnimChannel_Settings /*setting*/, + short *r_type) +{ + animrig::Binding *binding = static_cast(ale->data); + return GET_ACF_FLAG_PTR(binding->binding_flags, r_type); +} + +static bAnimChannelType ACF_ACTION_BINDING = { + /*channel_type_name*/ "Action Binding", + /*channel_role*/ ACHANNEL_ROLE_EXPANDER, + + /*get_backdrop_color*/ acf_generic_dataexpand_color, + /*get_channel_color*/ nullptr, + /*draw_backdrop*/ nullptr, + /*get_indent_level*/ acf_generic_indentation_0, + /*get_offset*/ acf_generic_group_offset, + + /*name*/ acf_action_binding_name, + /*name_prop*/ acf_action_binding_name_prop, + /*icon*/ acf_action_binding_icon, + + /*has_setting*/ acf_action_binding_setting_valid, + /*setting_flag*/ acf_action_binding_setting_flag, + /*setting_ptr*/ acf_action_binding_setting_ptr, +}; + +#endif // WITH_ANIM_BAKLAVA /* Object Action Expander ------------------------------------------- */ @@ -4379,9 +4490,11 @@ static void ANIM_init_channel_typeinfo_data() animchannelTypeInfo[type++] = &ACF_NLACURVE; /* NLA Control FCurve Channel */ #ifdef WITH_ANIM_BAKLAVA - animchannelTypeInfo[type++] = &ACF_FILLANIM; /* Object's Layered Action Expander */ + animchannelTypeInfo[type++] = &ACF_FILLANIM; /* Object's Layered Action Expander */ + animchannelTypeInfo[type++] = &ACF_ACTION_BINDING; /* Action Binding Expander */ #else animchannelTypeInfo[type++] = nullptr; + animchannelTypeInfo[type++] = nullptr; #endif animchannelTypeInfo[type++] = &ACF_FILLACTD; /* Object Action Expander */ animchannelTypeInfo[type++] = &ACF_FILLDRIVERS; /* Drivers Expander */ @@ -4426,6 +4539,8 @@ static void ANIM_init_channel_typeinfo_data() #ifdef WITH_ANIM_BAKLAVA BLI_assert_msg(animchannelTypeInfo[ANIMTYPE_FILLACT_LAYERED] == &ACF_FILLANIM, "ANIMTYPE_FILLACT_LAYERED does not match ACF_FILLANIM"); + BLI_assert_msg(animchannelTypeInfo[ANIMTYPE_ACTION_BINDING] == &ACF_ACTION_BINDING, + "ANIMTYPE_ACTION_BINDING does not match ACF_ACTION_BINDING"); #endif } } diff --git a/source/blender/editors/animation/anim_channels_edit.cc b/source/blender/editors/animation/anim_channels_edit.cc index 8cfc25bc46d..1b6f6aa9157 100644 --- a/source/blender/editors/animation/anim_channels_edit.cc +++ b/source/blender/editors/animation/anim_channels_edit.cc @@ -43,6 +43,8 @@ #include "BKE_screen.hh" #include "BKE_workspace.hh" +#include "ANIM_action.hh" + #include "DEG_depsgraph.hh" #include "DEG_depsgraph_build.hh" @@ -196,6 +198,7 @@ static bool get_channel_bounds(bAnimContext *ac, case ALE_ACT: case ALE_GROUP: case ALE_ACTION_LAYERED: + case ALE_ACTION_BINDING: case ALE_GREASE_PENCIL_DATA: case ALE_GREASE_PENCIL_GROUP: return false; @@ -309,6 +312,7 @@ void ANIM_set_active_channel(bAnimContext *ac, case ANIMTYPE_SUMMARY: case ANIMTYPE_SCENE: case ANIMTYPE_OBJECT: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_NLACONTROLS: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSNTREE: @@ -449,6 +453,7 @@ bool ANIM_is_active_channel(bAnimListElem *ale) case ANIMTYPE_SUMMARY: case ANIMTYPE_SCENE: case ANIMTYPE_OBJECT: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_NLACONTROLS: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_SHAPEKEY: @@ -549,7 +554,13 @@ static eAnimChannels_SetFlag anim_channels_selection_flag_for_toggle(const ListB return ACHANNEL_SETFLAG_CLEAR; } break; - + case ANIMTYPE_ACTION_BINDING: { + using namespace blender::animrig; + if (static_cast(ale->data)->is_selected()) { + return ACHANNEL_SETFLAG_CLEAR; + } + break; + } case ANIMTYPE_FILLACTD: /* Action Expander */ case ANIMTYPE_FILLACT_LAYERED: /* Animation Expander */ case ANIMTYPE_DSMAT: /* Datablock AnimData Expanders */ @@ -609,10 +620,41 @@ static eAnimChannels_SetFlag anim_channels_selection_flag_for_toggle(const ListB return ACHANNEL_SETFLAG_ADD; } +/** + * Update the selection state of `selectable_thing` based on `selectmode`. + * + * This is basically the C++ variant of the macro `ACHANNEL_SET_FLAG(thing, sel, selection_flag)`, + * except that this function doesn't require that the selectable thing has a member variable + * `flag`. Instead, it requires that it has two functions to query & set its selection state. + * + * \param selectable_thing: something with functions `set_selected(bool)` and `bool is_selected()`. + * \param selectmode the selection operation to perform. + */ +template +static void templated_selection_state_update(T &selectable_thing, + const eAnimChannels_SetFlag selectmode) +{ + switch (selectmode) { + case ACHANNEL_SETFLAG_CLEAR: + selectable_thing.set_selected(false); + break; + case ACHANNEL_SETFLAG_ADD: + case ACHANNEL_SETFLAG_EXTEND_RANGE: + selectable_thing.set_selected(true); + break; + case ACHANNEL_SETFLAG_INVERT: + case ACHANNEL_SETFLAG_TOGGLE: + selectable_thing.set_selected(!selectable_thing.is_selected()); + break; + } +} + static void anim_channels_select_set(bAnimContext *ac, const ListBase anim_data, eAnimChannels_SetFlag sel) { + using namespace blender; + /* Boolean to keep active channel status during range selection. */ const bool change_active = (sel != ACHANNEL_SETFLAG_EXTEND_RANGE); @@ -679,6 +721,11 @@ static void anim_channels_select_set(bAnimContext *ac, nlt->flag &= ~NLATRACK_ACTIVE; break; } + case ANIMTYPE_ACTION_BINDING: { + animrig::Binding *binding = static_cast(ale->data); + templated_selection_state_update(*binding, sel); + break; + } case ANIMTYPE_FILLACTD: /* Action Expander */ case ANIMTYPE_FILLACT_LAYERED: /* Animation Expander */ case ANIMTYPE_DSMAT: /* Datablock AnimData Expanders */ @@ -2327,6 +2374,7 @@ static int animchannels_delete_exec(bContext *C, wmOperator * /*op*/) case ANIMTYPE_GROUP: case ANIMTYPE_NLACONTROLS: case ANIMTYPE_FILLACT_LAYERED: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_FILLACTD: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSMAT: @@ -3090,10 +3138,6 @@ static void box_select_anim_channels(bAnimContext *ac, rcti *rect, short selectm /* filter data */ filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS); - if (!ANIM_animdata_can_have_greasepencil(eAnimCont_Types(ac->datatype))) { - filter |= ANIMFILTER_FCURVESONLY; - } - ANIM_animdata_filter( ac, &anim_data, eAnimFilter_Flags(filter), ac->data, eAnimCont_Types(ac->datatype)); @@ -3145,6 +3189,12 @@ static void box_select_anim_channels(bAnimContext *ac, rcti *rect, short selectm ACHANNEL_SET_FLAG(nlt, selectmode, NLATRACK_SELECTED); break; } + case ANIMTYPE_ACTION_BINDING: { + using namespace blender::animrig; + Binding *binding = static_cast(ale->data); + templated_selection_state_update(*binding, eAnimChannels_SetFlag(selectmode)); + break; + } case ANIMTYPE_NONE: case ANIMTYPE_ANIMDATA: case ANIMTYPE_SPECIALDATA__UNUSED: diff --git a/source/blender/editors/animation/anim_deps.cc b/source/blender/editors/animation/anim_deps.cc index ddeea49f91f..7e2b6214c39 100644 --- a/source/blender/editors/animation/anim_deps.cc +++ b/source/blender/editors/animation/anim_deps.cc @@ -298,6 +298,7 @@ void ANIM_sync_animchannels_to_data(const bContext *C) case ANIMTYPE_NLACONTROLS: case ANIMTYPE_NLACURVE: case ANIMTYPE_FILLACT_LAYERED: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_FILLACTD: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSMAT: diff --git a/source/blender/editors/animation/anim_filter.cc b/source/blender/editors/animation/anim_filter.cc index a0fe2b4782e..00f2799dff0 100644 --- a/source/blender/editors/animation/anim_filter.cc +++ b/source/blender/editors/animation/anim_filter.cc @@ -546,13 +546,13 @@ bool ANIM_animdata_can_have_greasepencil(const eAnimCont_Types type) * XXX: ale_statement stuff is really a hack for one special case. It shouldn't really be needed. */ #define ANIMCHANNEL_NEW_CHANNEL_FULL( \ - channel_data, channel_type, owner_id, fcurve_owner_id, ale_statement) \ + bmain, channel_data, channel_type, owner_id, fcurve_owner_id, ale_statement) \ if (filter_mode & ANIMFILTER_TMP_PEEK) { \ return 1; \ } \ { \ bAnimListElem *ale = make_new_animlistelem( \ - channel_data, channel_type, (ID *)owner_id, fcurve_owner_id); \ + bmain, channel_data, channel_type, (ID *)owner_id, fcurve_owner_id); \ if (ale) { \ BLI_addtail(anim_data, ale); \ items++; \ @@ -561,8 +561,8 @@ bool ANIM_animdata_can_have_greasepencil(const eAnimCont_Types type) } \ (void)0 -#define ANIMCHANNEL_NEW_CHANNEL(channel_data, channel_type, owner_id, fcurve_owner_id) \ - ANIMCHANNEL_NEW_CHANNEL_FULL(channel_data, channel_type, owner_id, fcurve_owner_id, {}) +#define ANIMCHANNEL_NEW_CHANNEL(bmain, channel_data, channel_type, owner_id, fcurve_owner_id) \ + ANIMCHANNEL_NEW_CHANNEL_FULL(bmain, channel_data, channel_type, owner_id, fcurve_owner_id, {}) /* ............................... */ @@ -613,10 +613,8 @@ static void key_data_from_adt(bAnimListElem &ale, AnimData *adt) /* this function allocates memory for a new bAnimListElem struct for the * provided animation channel-data. */ -static bAnimListElem *make_new_animlistelem(void *data, - const eAnim_ChannelType datatype, - ID *owner_id, - ID *fcurve_owner_id) +static bAnimListElem *make_new_animlistelem( + Main *bmain, void *data, const eAnim_ChannelType datatype, ID *owner_id, ID *fcurve_owner_id) { /* Only allocate memory if there is data to convert. */ if (!data) { @@ -630,6 +628,7 @@ static bAnimListElem *make_new_animlistelem(void *data, ale->data = data; ale->type = datatype; + ale->bmain = bmain; ale->id = owner_id; ale->adt = BKE_animdata_from_id(owner_id); ale->fcurve_owner_id = fcurve_owner_id; @@ -675,6 +674,16 @@ static bAnimListElem *make_new_animlistelem(void *data, ale->datatype = ALE_ACTION_LAYERED; break; } + case ANIMTYPE_ACTION_BINDING: { + animrig::Binding *binding = static_cast(data); + ale->flag = binding->binding_flags; + + BLI_assert_msg(GS(fcurve_owner_id->name) == ID_AC, "fcurve_owner_id should be an Action"); + /* ale->data = the binding itself, key_data = the Action. */ + ale->key_data = fcurve_owner_id; + ale->datatype = ALE_ACTION_BINDING; + break; + } case ANIMTYPE_FILLACTD: { bAction *act = (bAction *)data; @@ -1284,14 +1293,14 @@ static size_t animfilter_fcurves(bAnimContext *ac, if (UNLIKELY(fcurve_type == ANIMTYPE_NLACURVE)) { /* NLA Control Curve - Basically the same as normal F-Curves, * except we need to set some stuff differently */ - ANIMCHANNEL_NEW_CHANNEL_FULL(fcu, ANIMTYPE_NLACURVE, owner_id, fcurve_owner_id, { + ANIMCHANNEL_NEW_CHANNEL_FULL(ac->bmain, fcu, ANIMTYPE_NLACURVE, owner_id, fcurve_owner_id, { ale->owner = owner; /* strip */ ale->adt = nullptr; /* to prevent time mapping from causing problems */ }); } else { /* Normal FCurve */ - ANIMCHANNEL_NEW_CHANNEL(fcu, ANIMTYPE_FCURVE, owner_id, fcurve_owner_id); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, fcu, ANIMTYPE_FCURVE, owner_id, fcurve_owner_id); } } @@ -1299,9 +1308,24 @@ static size_t animfilter_fcurves(bAnimContext *ac, return items; } -static size_t animfilter_fcurves_span(bAnimContext * /*ac*/, +/** + * Add `bAnimListElem`s to `anim_data` for each F-Curve in `fcurves`. + * + * \param binding_handle The binding handle that these F-Curves animate. This is + * used later to look up the ID* of the user of the binding, which in turn is + * used to construct a suitable F-Curve label for in the channels list. + * + * \param owner_id The ID whose 'animdata->action' pointer was followed to get to + * these F-Curves. This ID may be animated by a different binding than referenced by + * `binding_handle`, so do _not_ treat this as "the ID animated by these F-Curves". + * + * \param fcurve_owner_id The ID that holds these F-Curves. Typically an Action, but can be any ID, + * for example in the case of drivers. + */ +static size_t animfilter_fcurves_span(bAnimContext *ac, ListBase * /*bAnimListElem*/ anim_data, Span fcurves, + const animrig::binding_handle_t binding_handle, const eAnimFilter_Flags filter_mode, ID *owner_id, ID *fcurve_owner_id) @@ -1309,6 +1333,10 @@ static size_t animfilter_fcurves_span(bAnimContext * /*ac*/, size_t num_items = 0; BLI_assert(owner_id); + const bool active_matters = filter_mode & ANIMFILTER_ACTIVE; + const bool selection_matters = filter_mode & (ANIMFILTER_SEL | ANIMFILTER_UNSEL); + const bool must_be_selected = filter_mode & ANIMFILTER_SEL; + for (FCurve *fcu : fcurves) { /* make_new_animlistelem will return nullptr when fcu == nullptr, and that's * going to cause problems. */ @@ -1321,8 +1349,28 @@ static size_t animfilter_fcurves_span(bAnimContext * /*ac*/, /* Found an animation channel, which is good enough for the 'TMP_PEEK' mode. */ return 1; } + if (selection_matters && bool(fcu->flag & FCURVE_SELECTED) != must_be_selected) { + continue; + } + if (active_matters && !(fcu->flag & FCURVE_ACTIVE)) { + continue; + } + + bAnimListElem *ale = make_new_animlistelem( + ac->bmain, fcu, ANIMTYPE_FCURVE, owner_id, fcurve_owner_id); + + /* bAnimListElem::binding_handle is exposed as int32_t and not as binding_handle_t, so better + * ensure that these are still equivalent. + * TODO: move to another part of the code. */ + static_assert( + std::is_same_v); + + /* Note that this might not be the same as ale->adt->binding_handle. The reason this F-Curve is + * shown could be because it's in the Action editor, showing ale->adt->action with _all_ + * bindings, and this F-Curve could be from a different binding than what's used by the owner + * of `ale->adt`. */ + ale->binding_handle = binding_handle; - bAnimListElem *ale = make_new_animlistelem(fcu, ANIMTYPE_FCURVE, owner_id, fcurve_owner_id); BLI_addtail(anim_data, ale); num_items++; } @@ -1410,7 +1458,7 @@ static size_t animfilter_act_group(bAnimContext *ac, /* filter selection of channel specially here again, * since may be open and not subject to previous test */ if (ANIMCHANNEL_SELOK(SEL_AGRP(agrp))) { - ANIMCHANNEL_NEW_CHANNEL(agrp, ANIMTYPE_GROUP, owner_id, &act->id); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, agrp, ANIMTYPE_GROUP, owner_id, &act->id); } } @@ -1424,6 +1472,82 @@ static size_t animfilter_act_group(bAnimContext *ac, return items; } +/** + * Add a channel for each Binding, with their FCurves when the Binding is expanded. + */ +static size_t animfilter_action_binding(bAnimContext *ac, + ListBase *anim_data, + animrig::Action &action, + animrig::Binding &binding, + const eAnimFilter_Flags filter_mode, + ID *owner_id) +{ + /* Don't include anything from this animation if it is linked in from another + * file, and we're getting stuff for editing... */ + if ((filter_mode & ANIMFILTER_FOREDIT) && + (ID_IS_LINKED(&action) || ID_IS_OVERRIDE_LIBRARY(&action))) + { + return 0; + } + + const bool selection_matters = filter_mode & (ANIMFILTER_SEL | ANIMFILTER_UNSEL); + const bool must_be_selected = filter_mode & ANIMFILTER_SEL; + const bool selection_ok_for_binding = !selection_matters || + binding.is_selected() == must_be_selected; + + int items = 0; + + /* Add a list element for the Binding itself, but only if in Action mode. The Dopesheet mode + * shouldn't display Bindings, as F-Curves are always shown in the context of the animated ID + * anyway. */ + const bool is_action_mode = (ac->mode == SACTCONT_ACTION); + const bool show_fcurves_only = (filter_mode & ANIMFILTER_FCURVESONLY); + const bool include_summary_channels = (filter_mode & ANIMFILTER_LIST_CHANNELS); + const bool show_binding_channel = (is_action_mode && selection_ok_for_binding && + !show_fcurves_only && include_summary_channels); + if (show_binding_channel) { + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, &binding, ANIMTYPE_ACTION_BINDING, owner_id, &action.id); + items++; + } + + /* If the 'list visible' flag is used, the expansion state of the Binding + * matters. Otherwise the sub-channels can always be listed. */ + const bool visible_only = (filter_mode & ANIMFILTER_LIST_VISIBLE); + const bool expansion_is_ok = !visible_only || !show_binding_channel || binding.is_expanded(); + + if (show_fcurves_only || expansion_is_ok) { + /* Add list elements for the F-Curves for this Binding. */ + Span fcurves = animrig::fcurves_for_animation(action, binding.handle); + items += animfilter_fcurves_span( + ac, anim_data, fcurves, binding.handle, filter_mode, owner_id, &action.id); + } + + return items; +} + +static size_t animfilter_action_bindings(bAnimContext *ac, + ListBase *anim_data, + animrig::Action &action, + const eAnimFilter_Flags filter_mode, + ID *owner_id) +{ + /* Don't include anything from this animation if it is linked in from another + * file, and we're getting stuff for editing... */ + if ((filter_mode & ANIMFILTER_FOREDIT) && + (ID_IS_LINKED(&action) || ID_IS_OVERRIDE_LIBRARY(&action))) + { + return 0; + } + + int num_items = 0; + for (animrig::Binding *binding : action.bindings()) { + BLI_assert(binding); + num_items += animfilter_action_binding(ac, anim_data, action, *binding, filter_mode, owner_id); + } + + return num_items; +} + static size_t animfilter_action(bAnimContext *ac, ListBase *anim_data, animrig::Action &action, @@ -1470,8 +1594,21 @@ static size_t animfilter_action(bAnimContext *ac, } /* For now we don't show layers anywhere, just the contained F-Curves. */ - Span fcurves = animrig::fcurves_for_animation(action, binding_handle); - return animfilter_fcurves_span(ac, anim_data, fcurves, filter_mode, owner_id, &action.id); + + /* Only show all Bindings in Action editor mode. Otherwise the F-Curves ought to be displayed + * underneath their animated ID anyway. */ + const bool is_action_mode = (ac->mode == SACTCONT_ACTION); + const bool show_all_bindings = (ac->ads->filterflag & ADS_FILTER_ALL_BINDINGS); + if (is_action_mode && show_all_bindings) { + return animfilter_action_bindings(ac, anim_data, action, filter_mode, owner_id); + } + + animrig::Binding *binding = action.binding_for_handle(binding_handle); + if (!binding) { + /* Can happen when an Action is assigned, but not a Binding. */ + return 0; + } + return animfilter_action_binding(ac, anim_data, action, *binding, filter_mode, owner_id); } /* Include NLA-Data for NLA-Editor: @@ -1507,7 +1644,7 @@ static size_t animfilter_nla(bAnimContext *ac, * then overwrite this with the real value - REVIEW THIS. */ ANIMCHANNEL_NEW_CHANNEL_FULL( - (void *)(&adt->action), ANIMTYPE_NLAACTION, owner_id, nullptr, { + ac->bmain, (void *)(&adt->action), ANIMTYPE_NLAACTION, owner_id, nullptr, { ale->data = adt->action ? adt->action : nullptr; }); } @@ -1561,7 +1698,7 @@ static size_t animfilter_nla(bAnimContext *ac, } /* add the track now that it has passed all our tests */ - ANIMCHANNEL_NEW_CHANNEL(nlt, ANIMTYPE_NLATRACK, owner_id, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, nlt, ANIMTYPE_NLATRACK, owner_id, nullptr); } } } @@ -1613,7 +1750,7 @@ static size_t animfilter_nla_controls(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* currently these channels cannot be selected, so they should be skipped */ if ((filter_mode & (ANIMFILTER_SEL | ANIMFILTER_UNSEL)) == 0) { - ANIMCHANNEL_NEW_CHANNEL(adt, ANIMTYPE_NLACONTROLS, owner_id, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, adt, ANIMTYPE_NLACONTROLS, owner_id, nullptr); } } @@ -1650,7 +1787,7 @@ static size_t animfilter_block_data(bAnimContext *ac, { /* AnimData */ /* specifically filter animdata block */ if (ANIMCHANNEL_SELOK(SEL_ANIMDATA(adt))) { - ANIMCHANNEL_NEW_CHANNEL(adt, ANIMTYPE_ANIMDATA, id, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, adt, ANIMTYPE_ANIMDATA, id, nullptr); } }, { /* NLA */ @@ -1670,12 +1807,20 @@ static size_t animfilter_block_data(bAnimContext *ac, items += animfilter_nla_controls(ac, anim_data, adt, filter_mode, id); }, { /* Keyframes from legacy Action. */ - items += animfilter_action( - ac, anim_data, adt->action->wrap(), adt->binding_handle, filter_mode, id); + items += animfilter_action(ac, + anim_data, + adt->action->wrap(), + adt->binding_handle, + eAnimFilter_Flags(filter_mode), + id); }, { /* Keyframes from layered Action. */ - items += animfilter_action( - ac, anim_data, adt->action->wrap(), adt->binding_handle, filter_mode, id); + items += animfilter_action(ac, + anim_data, + adt->action->wrap(), + adt->binding_handle, + eAnimFilter_Flags(filter_mode), + id); }); } @@ -1720,7 +1865,7 @@ static size_t animdata_filter_shapekey(bAnimContext *ac, /* TODO: consider 'active' too? */ /* owner-id here must be key so that the F-Curve can be resolved... */ - ANIMCHANNEL_NEW_CHANNEL(kb, ANIMTYPE_SHAPEKEY, key, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, kb, ANIMTYPE_SHAPEKEY, key, nullptr); } } } @@ -1740,7 +1885,7 @@ static size_t animdata_filter_shapekey(bAnimContext *ac, /* Check if this is a "KEY_NORMAL" type keyframe */ if (STREQ(fcu->rna_path, "eval_time") || BLI_str_endswith(fcu->rna_path, ".interpolation")) { - ANIMCHANNEL_NEW_CHANNEL(fcu, ANIMTYPE_FCURVE, (ID *)key, &action->id); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, fcu, ANIMTYPE_FCURVE, (ID *)key, &action->id); } } } @@ -1751,7 +1896,7 @@ static size_t animdata_filter_shapekey(bAnimContext *ac, if (key->adt) { if (filter_mode & ANIMFILTER_ANIMDATA) { if (ANIMCHANNEL_SELOK(SEL_ANIMDATA(key->adt))) { - ANIMCHANNEL_NEW_CHANNEL(key->adt, ANIMTYPE_ANIMDATA, key, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, key->adt, ANIMTYPE_ANIMDATA, key, nullptr); } } else if (key->adt->action) { @@ -1759,7 +1904,7 @@ static size_t animdata_filter_shapekey(bAnimContext *ac, anim_data, key->adt->action->wrap(), key->adt->binding_handle, - filter_mode, + eAnimFilter_Flags(filter_mode), (ID *)key); } } @@ -1771,7 +1916,7 @@ static size_t animdata_filter_shapekey(bAnimContext *ac, /* Helper for Grease Pencil - layers within a data-block. */ -static size_t animdata_filter_grease_pencil_layer(bAnimContext * /*ac*/, +static size_t animdata_filter_grease_pencil_layer(bAnimContext *ac, ListBase *anim_data, GreasePencil *grease_pencil, blender::bke::greasepencil::Layer &layer, @@ -1801,8 +1946,11 @@ static size_t animdata_filter_grease_pencil_layer(bAnimContext * /*ac*/, } /* Add layer channel. */ - ANIMCHANNEL_NEW_CHANNEL( - static_cast(&layer), ANIMTYPE_GREASE_PENCIL_LAYER, grease_pencil, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, + static_cast(&layer), + ANIMTYPE_GREASE_PENCIL_LAYER, + grease_pencil, + nullptr); return items; } @@ -1850,8 +1998,11 @@ static size_t animdata_filter_grease_pencil_layer_node_recursive( if ((filter_mode & ANIMFILTER_LIST_CHANNELS) && !skip_node) { /* Add data block container (if for drawing, and it contains sub-channels). */ - ANIMCHANNEL_NEW_CHANNEL( - static_cast(&node), ANIMTYPE_GREASE_PENCIL_LAYER_GROUP, grease_pencil, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, + static_cast(&node), + ANIMTYPE_GREASE_PENCIL_LAYER_GROUP, + grease_pencil, + nullptr); } /* Add the list of collected channels. */ @@ -1917,7 +2068,7 @@ static size_t animdata_filter_gpencil_layers_data_legacy(bAnimContext *ac, } /* add to list */ - ANIMCHANNEL_NEW_CHANNEL(gpl, ANIMTYPE_GPLAYER, gpd, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, gpl, ANIMTYPE_GPLAYER, gpd, nullptr); } return items; @@ -1939,7 +2090,7 @@ static size_t animdata_filter_grease_pencil_data(bAnimContext *ac, if (filter_mode & ANIMFILTER_ANIMDATA) { /* Just add data block container. */ ANIMCHANNEL_NEW_CHANNEL( - grease_pencil, ANIMTYPE_GREASE_PENCIL_DATABLOCK, grease_pencil, nullptr); + ac->bmain, grease_pencil, ANIMTYPE_GREASE_PENCIL_DATABLOCK, grease_pencil, nullptr); } else { ListBase tmp_data = {nullptr, nullptr}; @@ -1962,7 +2113,7 @@ static size_t animdata_filter_grease_pencil_data(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* Add data block container (if for drawing, and it contains sub-channels). */ ANIMCHANNEL_NEW_CHANNEL( - grease_pencil, ANIMTYPE_GREASE_PENCIL_DATABLOCK, grease_pencil, nullptr); + ac->bmain, grease_pencil, ANIMTYPE_GREASE_PENCIL_DATABLOCK, grease_pencil, nullptr); } /* Add the list of collected channels. */ @@ -2058,7 +2209,7 @@ static size_t animdata_filter_ds_gpencil(bAnimContext *ac, /* check if filtering by active status */ /* XXX: active check here needs checking */ if (ANIMCHANNEL_ACTIVEOK(gpd)) { - ANIMCHANNEL_NEW_CHANNEL(gpd, ANIMTYPE_DSGPENCIL, gpd, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, gpd, ANIMTYPE_DSGPENCIL, gpd, nullptr); } } @@ -2096,7 +2247,7 @@ static size_t animdata_filter_ds_cachefile(bAnimContext *ac, /* check if filtering by active status */ /* XXX: active check here needs checking */ if (ANIMCHANNEL_ACTIVEOK(cache_file)) { - ANIMCHANNEL_NEW_CHANNEL(cache_file, ANIMTYPE_DSCACHEFILE, cache_file, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, cache_file, ANIMTYPE_DSCACHEFILE, cache_file, nullptr); } } @@ -2111,7 +2262,8 @@ static size_t animdata_filter_ds_cachefile(bAnimContext *ac, } /* Helper for Mask Editing - mask layers */ -static size_t animdata_filter_mask_data(ListBase *anim_data, +static size_t animdata_filter_mask_data(bAnimContext *ac, + ListBase *anim_data, Mask *mask, const eAnimFilter_Flags filter_mode) { @@ -2131,14 +2283,14 @@ static size_t animdata_filter_mask_data(ListBase *anim_data, continue; } - ANIMCHANNEL_NEW_CHANNEL(masklay, ANIMTYPE_MASKLAYER, mask, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, masklay, ANIMTYPE_MASKLAYER, mask, nullptr); } return items; } /* Grab all mask data */ -static size_t animdata_filter_mask(Main *bmain, +static size_t animdata_filter_mask(bAnimContext *ac, ListBase *anim_data, void * /*data*/, eAnimFilter_Flags filter_mode) @@ -2147,7 +2299,7 @@ static size_t animdata_filter_mask(Main *bmain, /* For now, grab mask data-blocks directly from main. */ /* XXX: this is not good... */ - LISTBASE_FOREACH (Mask *, mask, &bmain->masks) { + LISTBASE_FOREACH (Mask *, mask, &ac->bmain->masks) { ListBase tmp_data = {nullptr, nullptr}; size_t tmp_items = 0; @@ -2159,7 +2311,7 @@ static size_t animdata_filter_mask(Main *bmain, /* add mask animation channels */ if (!(filter_mode & ANIMFILTER_FCURVESONLY)) { BEGIN_ANIMFILTER_SUBCHANNELS (EXPANDED_MASK(mask)) { - tmp_items += animdata_filter_mask_data(&tmp_data, mask, filter_mode); + tmp_items += animdata_filter_mask_data(ac, &tmp_data, mask, filter_mode); } END_ANIMFILTER_SUBCHANNELS; } @@ -2172,7 +2324,7 @@ static size_t animdata_filter_mask(Main *bmain, /* include data-expand widget first */ if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* add mask data-block as channel too (if for drawing, and it has layers) */ - ANIMCHANNEL_NEW_CHANNEL(mask, ANIMTYPE_MASKDATABLOCK, nullptr, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, mask, ANIMTYPE_MASKDATABLOCK, nullptr, nullptr); } /* now add the list of collected channels */ @@ -2210,7 +2362,7 @@ static size_t animdata_filter_ds_nodetree_group(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(ntree)) { - ANIMCHANNEL_NEW_CHANNEL(ntree, ANIMTYPE_DSNTREE, owner_id, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, ntree, ANIMTYPE_DSNTREE, owner_id, nullptr); } } @@ -2298,7 +2450,7 @@ static size_t animdata_filter_ds_linestyle(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(linestyle)) { - ANIMCHANNEL_NEW_CHANNEL(linestyle, ANIMTYPE_DSLINESTYLE, sce, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, linestyle, ANIMTYPE_DSLINESTYLE, sce, nullptr); } } @@ -2346,7 +2498,7 @@ static size_t animdata_filter_ds_texture( if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(tex)) { - ANIMCHANNEL_NEW_CHANNEL(tex, ANIMTYPE_DSTEX, owner_id, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, tex, ANIMTYPE_DSTEX, owner_id, nullptr); } } @@ -2438,7 +2590,7 @@ static size_t animdata_filter_ds_material(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(ma)) { - ANIMCHANNEL_NEW_CHANNEL(ma, ANIMTYPE_DSMAT, ma, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, ma, ANIMTYPE_DSMAT, ma, nullptr); } } @@ -2604,7 +2756,7 @@ static size_t animdata_filter_ds_particles(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(psys->part)) { - ANIMCHANNEL_NEW_CHANNEL(psys->part, ANIMTYPE_DSPART, psys->part, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, psys->part, ANIMTYPE_DSPART, psys->part, nullptr); } } @@ -2796,7 +2948,7 @@ static size_t animdata_filter_ds_obdata(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(iat)) { - ANIMCHANNEL_NEW_CHANNEL(iat, type, iat, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, iat, type, iat, nullptr); } } @@ -2830,7 +2982,7 @@ static size_t animdata_filter_ds_keyanim( /* include key-expand widget first */ if (filter_mode & ANIMFILTER_LIST_CHANNELS) { if (ANIMCHANNEL_ACTIVEOK(key)) { - ANIMCHANNEL_NEW_CHANNEL(key, ANIMTYPE_DSSKEY, ob, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, key, ANIMTYPE_DSSKEY, ob, nullptr); } } @@ -2895,7 +3047,7 @@ static size_t animdata_filter_ds_obanim(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { if (type != ANIMTYPE_NONE) { /* NOTE: active-status (and the associated checks) don't apply here... */ - ANIMCHANNEL_NEW_CHANNEL(cdata, type, ob, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, cdata, type, ob, nullptr); } } @@ -2982,7 +3134,7 @@ static size_t animdata_filter_dopesheet_ob(bAnimContext *ac, if (ANIMCHANNEL_SELOK((base->flag & BASE_SELECTED))) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(ob)) { - ANIMCHANNEL_NEW_CHANNEL(base, ANIMTYPE_OBJECT, ob, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, base, ANIMTYPE_OBJECT, ob, nullptr); } } } @@ -3022,7 +3174,7 @@ static size_t animdata_filter_ds_world( if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(wo)) { - ANIMCHANNEL_NEW_CHANNEL(wo, ANIMTYPE_DSWOR, sce, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, wo, ANIMTYPE_DSWOR, sce, nullptr); } } @@ -3086,7 +3238,7 @@ static size_t animdata_filter_ds_scene(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { if (type != ANIMTYPE_NONE) { /* NOTE: active-status (and the associated checks) don't apply here... */ - ANIMCHANNEL_NEW_CHANNEL(cdata, type, sce, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, cdata, type, sce, nullptr); } } @@ -3152,7 +3304,7 @@ static size_t animdata_filter_dopesheet_scene(bAnimContext *ac, /* check if filtering by selection */ if (ANIMCHANNEL_SELOK((sce->flag & SCE_DS_SELECTED))) { /* NOTE: active-status doesn't matter for this! */ - ANIMCHANNEL_NEW_CHANNEL(sce, ANIMTYPE_SCENE, sce, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, sce, ANIMTYPE_SCENE, sce, nullptr); } } @@ -3186,7 +3338,7 @@ static size_t animdata_filter_ds_movieclip(bAnimContext *ac, if (filter_mode & ANIMFILTER_LIST_CHANNELS) { /* check if filtering by active status */ if (ANIMCHANNEL_ACTIVEOK(clip)) { - ANIMCHANNEL_NEW_CHANNEL(clip, ANIMTYPE_DSMCLIP, clip, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, clip, ANIMTYPE_DSMCLIP, clip, nullptr); } } /* now add the list of collected channels */ @@ -3474,7 +3626,7 @@ static short animdata_filter_dopesheet_summary(bAnimContext *ac, * - only useful for DopeSheet/Action/etc. editors where it is actually useful */ if ((filter_mode & ANIMFILTER_LIST_CHANNELS) && (ac->ads->filterflag & ADS_FILTER_SUMMARY)) { - bAnimListElem *ale = make_new_animlistelem(ac, ANIMTYPE_SUMMARY, nullptr, nullptr); + bAnimListElem *ale = make_new_animlistelem(ac->bmain, ac, ANIMTYPE_SUMMARY, nullptr, nullptr); if (ale) { BLI_addtail(anim_data, ale); (*items)++; @@ -3597,7 +3749,7 @@ size_t ANIM_animdata_filter(bAnimContext *ac, ListBase *anim_data, const eAnimFilter_Flags filter_mode, void *data, - eAnimCont_Types datatype) + const eAnimCont_Types datatype) { if (!data || !anim_data) { return 0; @@ -3615,10 +3767,11 @@ size_t ANIM_animdata_filter(bAnimContext *ac, UNUSED_VARS_NDEBUG(ads); /* specially check for AnimData filter, see #36687. */ + /* TODO: see how this interacts with the new layered Actions. */ if (UNLIKELY(filter_mode & ANIMFILTER_ANIMDATA)) { /* all channels here are within the same AnimData block, hence this special case */ if (LIKELY(obact->adt)) { - ANIMCHANNEL_NEW_CHANNEL(obact->adt, ANIMTYPE_ANIMDATA, (ID *)obact, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, obact->adt, ANIMTYPE_ANIMDATA, (ID *)obact, nullptr); } } else { @@ -3646,7 +3799,7 @@ size_t ANIM_animdata_filter(bAnimContext *ac, if (UNLIKELY(filter_mode & ANIMFILTER_ANIMDATA)) { /* all channels here are within the same AnimData block, hence this special case */ if (LIKELY(key->adt)) { - ANIMCHANNEL_NEW_CHANNEL(key->adt, ANIMTYPE_ANIMDATA, (ID *)key, nullptr); + ANIMCHANNEL_NEW_CHANNEL(ac->bmain, key->adt, ANIMTYPE_ANIMDATA, (ID *)key, nullptr); } } else { @@ -3669,7 +3822,7 @@ size_t ANIM_animdata_filter(bAnimContext *ac, } case ANIMCONT_MASK: { if (animdata_filter_dopesheet_summary(ac, anim_data, filter_mode, &items)) { - items = animdata_filter_mask(ac->bmain, anim_data, data, filter_mode); + items = animdata_filter_mask(ac, anim_data, data, filter_mode); } break; } diff --git a/source/blender/editors/animation/anim_filter_test.cc b/source/blender/editors/animation/anim_filter_test.cc new file mode 100644 index 00000000000..787b32f0ec5 --- /dev/null +++ b/source/blender/editors/animation/anim_filter_test.cc @@ -0,0 +1,277 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "ANIM_action.hh" +#include "ANIM_fcurve.hh" + +#include "BKE_action.hh" +#include "BKE_anim_data.hh" +#include "BKE_fcurve.hh" +#include "BKE_global.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 "DNA_space_types.h" + +#include "ED_anim_api.hh" + +#include "BLI_listbase.h" + +#include "CLG_log.h" +#include "testing/testing.h" + +namespace blender::animrig::tests { +class ActionFilterTest : public testing::Test { + public: + Main *bmain; + Action *action; + 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(); + G_MAIN = bmain; /* For BKE_animdata_free(). */ + + action = &static_cast(BKE_id_new(bmain, ID_AC, "ACÄnimåtië"))->wrap(); + 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); + G_MAIN = nullptr; + } +}; + +TEST_F(ActionFilterTest, bindings_expanded_or_not) +{ + Binding &bind_cube = action->binding_add(); + Binding &bind_suzanne = action->binding_add(); + ASSERT_TRUE(action->assign_id(&bind_cube, cube->id)); + ASSERT_TRUE(action->assign_id(&bind_suzanne, suzanne->id)); + + Layer &layer = action->layer_add("Kübus layer"); + KeyframeStrip &key_strip = layer.strip_add(Strip::Type::Keyframe).as(); + + /* Create multiple FCurves for multiple Bindings. */ + const KeyframeSettings settings = get_keyframe_settings(false); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_cube, {"location", 0}, {1.0f, 0.25f}, settings)); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_cube, {"location", 1}, {1.0f, 0.25f}, settings)); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_suzanne, {"location", 0}, {1.0f, 0.25f}, settings)); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_suzanne, {"location", 1}, {1.0f, 0.25f}, settings)); + + FCurve *fcu_cube_loc_x = key_strip.fcurve_find(bind_cube, {"location", 0}); + FCurve *fcu_cube_loc_y = key_strip.fcurve_find(bind_cube, {"location", 1}); + ASSERT_NE(nullptr, fcu_cube_loc_x); + ASSERT_NE(nullptr, fcu_cube_loc_y); + + /* Mock an bAnimContext for the Animation editor, with the above Animation showing. */ + SpaceAction saction = {0}; + saction.action = action; + saction.action_binding_handle = bind_cube.handle; + saction.ads.filterflag = ADS_FILTER_ALL_BINDINGS; + + bAnimContext ac = {0}; + ac.datatype = ANIMCONT_ACTION; + ac.data = action; + ac.spacetype = SPACE_ACTION; + ac.sl = reinterpret_cast(&saction); + ac.obact = cube; + ac.ads = &saction.ads; + + { /* Test with collapsed bindings. */ + bind_cube.set_expanded(false); + bind_suzanne.set_expanded(false); + + /* This should produce 2 bindings and no FCurves. */ + ListBase anim_data = {nullptr, nullptr}; + eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | + ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS | + ANIMFILTER_LIST_CHANNELS); + const int num_entries = ANIM_animdata_filter( + &ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype)); + EXPECT_EQ(2, num_entries); + EXPECT_EQ(2, BLI_listbase_count(&anim_data)); + + ASSERT_GE(num_entries, 1) + << "Missing 1st ANIMTYPE_ACTION_BINDING entry, stopping to prevent crash"; + const bAnimListElem *first_ale = static_cast(BLI_findlink(&anim_data, 0)); + EXPECT_EQ(ANIMTYPE_ACTION_BINDING, first_ale->type); + EXPECT_EQ(ALE_ACTION_BINDING, first_ale->datatype); + EXPECT_EQ(&cube->id, first_ale->id) << "id should be the animated ID (" << cube->id.name + << ") but is (" << first_ale->id->name << ")"; + EXPECT_EQ(cube->adt, first_ale->adt) << "adt should be the animated ID's animation data"; + EXPECT_EQ(&action->id, first_ale->fcurve_owner_id) << "fcurve_owner_id should be the Action"; + EXPECT_EQ(&action->id, first_ale->key_data) << "key_data should be the Action"; + EXPECT_EQ(&bind_cube, first_ale->data); + EXPECT_EQ(bind_cube.binding_flags, first_ale->flag); + + ASSERT_GE(num_entries, 2) + << "Missing 2nd ANIMTYPE_ACTION_BINDING entry, stopping to prevent crash"; + const bAnimListElem *second_ale = static_cast(BLI_findlink(&anim_data, 1)); + EXPECT_EQ(ANIMTYPE_ACTION_BINDING, second_ale->type); + EXPECT_EQ(&bind_suzanne, second_ale->data); + /* Assume the rest is set correctly, as it's the same code as tested above. */ + + ANIM_animdata_freelist(&anim_data); + } + + { /* Test with one expanded and one collapsed binding. */ + bind_cube.set_expanded(true); + bind_suzanne.set_expanded(false); + + /* This should produce 2 bindings and 2 FCurves. */ + ListBase anim_data = {nullptr, nullptr}; + eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | + ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS | + ANIMFILTER_LIST_CHANNELS); + const int num_entries = ANIM_animdata_filter( + &ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype)); + EXPECT_EQ(4, num_entries); + EXPECT_EQ(4, BLI_listbase_count(&anim_data)); + + /* First should be Cube binding. */ + ASSERT_GE(num_entries, 1) << "Missing 1st ale, stopping to prevent crash"; + const bAnimListElem *ale = static_cast(BLI_findlink(&anim_data, 0)); + EXPECT_EQ(ANIMTYPE_ACTION_BINDING, ale->type); + EXPECT_EQ(&bind_cube, ale->data); + + /* After that the Cube's FCurves. */ + ASSERT_GE(num_entries, 2) << "Missing 2nd ale, stopping to prevent crash"; + ale = static_cast(BLI_findlink(&anim_data, 1)); + EXPECT_EQ(ANIMTYPE_FCURVE, ale->type); + EXPECT_EQ(fcu_cube_loc_x, ale->data); + EXPECT_EQ(bind_cube.handle, ale->binding_handle); + + ASSERT_GE(num_entries, 3) << "Missing 3rd ale, stopping to prevent crash"; + ale = static_cast(BLI_findlink(&anim_data, 2)); + EXPECT_EQ(ANIMTYPE_FCURVE, ale->type); + EXPECT_EQ(fcu_cube_loc_y, ale->data); + EXPECT_EQ(bind_cube.handle, ale->binding_handle); + + /* And finally the Suzanne binding. */ + ASSERT_GE(num_entries, 4) << "Missing 4th ale, stopping to prevent crash"; + ale = static_cast(BLI_findlink(&anim_data, 3)); + EXPECT_EQ(ANIMTYPE_ACTION_BINDING, ale->type); + EXPECT_EQ(&bind_suzanne, ale->data); + + ANIM_animdata_freelist(&anim_data); + } + + { /* Test one expanded and one collapsed binding, and one Binding and one FCurve selected. */ + bind_cube.set_expanded(true); + bind_cube.set_selected(false); + bind_suzanne.set_expanded(false); + bind_suzanne.set_selected(true); + + fcu_cube_loc_x->flag &= ~FCURVE_SELECTED; + fcu_cube_loc_y->flag |= FCURVE_SELECTED; + + /* This should produce 1 binding and 1 FCurve. */ + ListBase anim_data = {nullptr, nullptr}; + eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | + ANIMFILTER_SEL | ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS | + ANIMFILTER_LIST_CHANNELS); + const int num_entries = ANIM_animdata_filter( + &ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype)); + EXPECT_EQ(2, num_entries); + EXPECT_EQ(2, BLI_listbase_count(&anim_data)); + + /* First should be Cube's selected FCurve. */ + const bAnimListElem *ale = static_cast(BLI_findlink(&anim_data, 0)); + EXPECT_EQ(ANIMTYPE_FCURVE, ale->type); + EXPECT_EQ(fcu_cube_loc_y, ale->data); + + /* Second the Suzanne binding, as that's the only selected binding. */ + ale = static_cast(BLI_findlink(&anim_data, 1)); + EXPECT_EQ(ANIMTYPE_ACTION_BINDING, ale->type); + EXPECT_EQ(&bind_suzanne, ale->data); + + ANIM_animdata_freelist(&anim_data); + } +} + +TEST_F(ActionFilterTest, layered_action_active_fcurves) +{ + Binding &bind_cube = action->binding_add(); + /* The Action+Binding has to be assigned to what the bAnimContext thinks is the active Object. + * See the BLI_assert_msg() call in the ANIMCONT_ACTION case of ANIM_animdata_filter(). */ + ASSERT_TRUE(action->assign_id(&bind_cube, cube->id)); + + Layer &layer = action->layer_add("Kübus layer"); + KeyframeStrip &key_strip = layer.strip_add(Strip::Type::Keyframe).as(); + + /* Create multiple FCurves. */ + const KeyframeSettings settings = get_keyframe_settings(false); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_cube, {"location", 0}, {1.0f, 0.25f}, settings)); + ASSERT_EQ(SingleKeyingResult::SUCCESS, + key_strip.keyframe_insert(bind_cube, {"location", 1}, {1.0f, 0.25f}, settings)); + + /* Set one F-Curve as the active one, and the other as inactive. The latter is necessary because + * by default the first curve is automatically marked active, but that's too trivial a test case + * (it's too easy to mistakenly just return the first-seen F-Curve). */ + FCurve *fcurve_active = key_strip.fcurve_find(bind_cube, {"location", 1}); + fcurve_active->flag |= FCURVE_ACTIVE; + FCurve *fcurve_other = key_strip.fcurve_find(bind_cube, {"location", 0}); + fcurve_other->flag &= ~FCURVE_ACTIVE; + + /* Mock an bAnimContext for the Action editor. */ + SpaceAction saction = {0}; + saction.action = action; + saction.action_binding_handle = bind_cube.handle; + saction.ads.filterflag = ADS_FILTER_ALL_BINDINGS; + + bAnimContext ac = {0}; + ac.datatype = ANIMCONT_ACTION; + ac.data = action; + ac.spacetype = SPACE_ACTION; + ac.sl = reinterpret_cast(&saction); + ac.obact = cube; + ac.ads = &saction.ads; + + { + /* This should produce just the active F-Curve. */ + ListBase anim_data = {nullptr, nullptr}; + eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | + ANIMFILTER_FCURVESONLY | ANIMFILTER_ACTIVE); + const int num_entries = ANIM_animdata_filter( + &ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype)); + EXPECT_EQ(1, num_entries); + EXPECT_EQ(1, BLI_listbase_count(&anim_data)); + + const bAnimListElem *first_ale = static_cast(BLI_findlink(&anim_data, 0)); + EXPECT_EQ(ANIMTYPE_FCURVE, first_ale->type); + EXPECT_EQ(ALE_FCURVE, first_ale->datatype); + EXPECT_EQ(fcurve_active, first_ale->data); + + ANIM_animdata_freelist(&anim_data); + } +} + +} // namespace blender::animrig::tests diff --git a/source/blender/editors/animation/anim_ipo_utils.cc b/source/blender/editors/animation/anim_ipo_utils.cc index 8b72dfaa88e..c822b9467aa 100644 --- a/source/blender/editors/animation/anim_ipo_utils.cc +++ b/source/blender/editors/animation/anim_ipo_utils.cc @@ -28,8 +28,14 @@ #include "ED_anim_api.hh" +#include "ANIM_action.hh" + +#include "fmt/format.h" + #include +struct StructRNA; + /* ----------------------- Getter functions ----------------------- */ int getname_anim_fcurve(char *name, ID *id, FCurve *fcu) @@ -39,6 +45,8 @@ int getname_anim_fcurve(char *name, ID *id, FCurve *fcu) /* Handle some nullptr cases. */ if (name == nullptr) { + /* A 'get name' function should be able to get the name, otherwise it's a bug. */ + BLI_assert_unreachable(); return 0; } if (fcu == nullptr) { @@ -198,6 +206,79 @@ int getname_anim_fcurve(char *name, ID *id, FCurve *fcu) return RNA_struct_ui_icon(ptr.type); } +std::string getname_anim_fcurve_bound(Main &bmain, + const blender::animrig::Binding &binding, + FCurve &fcurve) +{ + /* TODO: Refactor to avoid this variable. */ + constexpr size_t name_maxncpy = 256; + char name_buffer[name_maxncpy]; + name_buffer[0] = '\0'; + + /* Check the Binding's users to see if we can find an ID* that can resolve the F-Curve. */ + for (ID *user : binding.users(bmain)) { + const int icon = getname_anim_fcurve(name_buffer, user, &fcurve); + if (icon) { + /* Managed to find a name! */ + return name_buffer; + } + } + + if (!binding.users(bmain).is_empty()) { + /* This binding is assigned to at least one ID, and still the property it animates could not be + * found. There is no use in continuing. */ + fcurve.flag |= FCURVE_DISABLED; + return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index); + } + + /* If this part of the code is hit, the binding is not assigned to anything. The remainder of + * this function is all a best-effort attempt. Because of that, it will not set the + * FCURVE_DISABLED flag on the F-Curve, as having unassigned animation data is not an error (and + * that flag indicates an error). */ + + /* Fall back to the ID type of the binding for simple properties. */ + if (!binding.has_idtype()) { + /* The Binding has never been assigned to any ID, so we don't even know what type of ID it is + * meant for. */ + return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index); + } + + if (blender::StringRef(fcurve.rna_path).find(".") != blender::StringRef::not_found) { + /* Not a simple property, so bail out. This needs path resolution, which needs an ID*. */ + return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index); + } + + /* Find the StructRNA for this Binding's ID type. */ + StructRNA *srna = ID_code_to_RNA_type(binding.idtype); + if (!srna) { + return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index); + } + + /* Find the property. */ + PropertyRNA *prop = RNA_struct_type_find_property(srna, fcurve.rna_path); + if (!prop) { + return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index); + } + + /* Property Name is straightforward */ + const char *propname = RNA_property_ui_name(prop); + + /* Array Index - only if applicable */ + if (!RNA_property_array_check(prop)) { + return propname; + } + + std::string arrayname = ""; + char c = RNA_property_array_item_char(prop, fcurve.array_index); + if (c) { + arrayname = std::string(1, c); + } + else { + arrayname = fmt::format("[{}]", fcurve.array_index); + } + return arrayname + " " + propname; +} + /* ------------------------------- Color Codes for F-Curve Channels ---------------------------- */ /* step between the major distinguishable color bands of the primary colors */ diff --git a/source/blender/editors/animation/keyframes_draw.cc b/source/blender/editors/animation/keyframes_draw.cc index 00196a52e86..cae9368ab46 100644 --- a/source/blender/editors/animation/keyframes_draw.cc +++ b/source/blender/editors/animation/keyframes_draw.cc @@ -35,6 +35,8 @@ #include "ANIM_action.hh" +using namespace blender; + /* *************************** Keyframe Drawing *************************** */ void draw_keyframe_shape(const float x, @@ -387,6 +389,7 @@ enum class ChannelType { OBJECT, FCURVE, ACTION_LAYERED, + ACTION_BINDING, ACTION_LEGACY, ACTION_GROUP, GREASE_PENCIL_CELS, @@ -406,6 +409,7 @@ struct ChannelListElement { eSAction_Flag saction_flag; bool channel_locked; + /* TODO: check which of these can be put into a `union`: */ bAnimContext *ac; bDopeSheet *ads; Scene *sce; @@ -413,6 +417,7 @@ struct ChannelListElement { AnimData *adt; FCurve *fcu; bAction *act; + animrig::Binding *action_binding; bActionGroup *agrp; bGPDlayer *gpl; const GreasePencilLayer *grease_pencil_layer; @@ -444,6 +449,17 @@ static void build_channel_keylist(ChannelListElement *elem, blender::float2 rang action_to_keylist(elem->adt, elem->act, elem->keylist, elem->saction_flag, range); break; } + case ChannelType::ACTION_BINDING: { + BLI_assert(elem->act); + BLI_assert(elem->action_binding); + action_binding_to_keylist(elem->adt, + elem->act->wrap(), + elem->action_binding->handle, + elem->keylist, + elem->saction_flag, + range); + break; + } case ChannelType::ACTION_LEGACY: { action_to_keylist(elem->adt, elem->act, elem->keylist, elem->saction_flag, range); break; @@ -732,6 +748,25 @@ void ED_add_action_layered_channel(ChannelDrawList *channel_list, draw_elem->channel_locked = locked; } +void ED_add_action_binding_channel(ChannelDrawList *channel_list, + AnimData *adt, + animrig::Action &action, + animrig::Binding &binding, + const float ypos, + const float yscale_fac, + int saction_flag) +{ + const bool locked = (ID_IS_LINKED(&action) || ID_IS_OVERRIDE_LIBRARY(&action)); + saction_flag &= ~SACTION_SHOW_EXTREMES; + + ChannelListElement *draw_elem = channel_list_add_element( + channel_list, ChannelType::ACTION_BINDING, ypos, yscale_fac, eSAction_Flag(saction_flag)); + draw_elem->adt = adt; + draw_elem->act = &action; + draw_elem->action_binding = &binding; + draw_elem->channel_locked = locked; +} + void ED_add_action_channel(ChannelDrawList *channel_list, AnimData *adt, bAction *act, diff --git a/source/blender/editors/animation/keyframes_edit.cc b/source/blender/editors/animation/keyframes_edit.cc index 41c385f059c..2f98ba84042 100644 --- a/source/blender/editors/animation/keyframes_edit.cc +++ b/source/blender/editors/animation/keyframes_edit.cc @@ -168,13 +168,13 @@ static short agrp_keyframes_loop(KeyframeEditData *ked, #ifdef WITH_ANIM_BAKLAVA -/* Loop over all keyframes in the Animation. */ -static short anim_keyframes_loop(KeyframeEditData *ked, - animrig::Action &anim, - animrig::Binding *binding, - KeyframeEditFunc key_ok, - KeyframeEditFunc key_cb, - FcuEditFunc fcu_cb) +/* Loop over all keyframes in the layered Action. */ +static short action_layered_keyframes_loop(KeyframeEditData *ked, + animrig::Action &anim, + animrig::Binding *binding, + KeyframeEditFunc key_ok, + KeyframeEditFunc key_cb, + FcuEditFunc fcu_cb) { if (!binding) { /* Valid situation, and will not have any FCurves. */ @@ -193,11 +193,11 @@ static short anim_keyframes_loop(KeyframeEditData *ked, #endif /* This function is used to loop over the keyframe data in an Action */ -static short act_keyframes_loop(KeyframeEditData *ked, - bAction *act, - KeyframeEditFunc key_ok, - KeyframeEditFunc key_cb, - FcuEditFunc fcu_cb) +static short action_legacy_keyframes_loop(KeyframeEditData *ked, + bAction *act, + KeyframeEditFunc key_ok, + KeyframeEditFunc key_cb, + FcuEditFunc fcu_cb) { /* sanity check */ if (act == nullptr) { @@ -416,20 +416,31 @@ short ANIM_animchannel_keyframes_loop(KeyframeEditData *ked, */ case ALE_GROUP: /* action group */ return agrp_keyframes_loop(ked, (bActionGroup *)ale->data, key_ok, key_cb, fcu_cb); - case ALE_ACTION_LAYERED: { /* Animation data-block. */ + case ALE_ACTION_LAYERED: { /* Layered Action. */ #ifdef WITH_ANIM_BAKLAVA /* This assumes that the ALE_ACTION_LAYERED channel is shown in the dopesheet context, * underneath the data-block that owns `ale->adt`. So that means that the loop is limited to * the keys that belong to that binding. */ animrig::Action &anim = static_cast(ale->key_data)->wrap(); animrig::Binding *binding = anim.binding_for_handle(ale->adt->binding_handle); - return anim_keyframes_loop(ked, anim, binding, key_ok, key_cb, fcu_cb); + return action_layered_keyframes_loop(ked, anim, binding, key_ok, key_cb, fcu_cb); #else return 0; #endif } - case ALE_ACT: /* action */ - return act_keyframes_loop(ked, (bAction *)ale->key_data, key_ok, key_cb, fcu_cb); + case ALE_ACTION_BINDING: { +#ifdef WITH_ANIM_BAKLAVA + animrig::Action *action = static_cast(ale->key_data); + BLI_assert(action); + animrig::Binding *binding = static_cast(ale->data); + return action_layered_keyframes_loop(ked, *action, binding, key_ok, key_cb, fcu_cb); +#else + return 0; +#endif + } + + case ALE_ACT: /* Legacy Action. */ + return action_legacy_keyframes_loop(ked, (bAction *)ale->key_data, key_ok, key_cb, fcu_cb); case ALE_OB: /* object */ return ob_keyframes_loop(ked, ads, (Object *)ale->key_data, key_ok, key_cb, fcu_cb); case ALE_SCE: /* scene */ @@ -475,11 +486,12 @@ short ANIM_animchanneldata_keyframes_loop(KeyframeEditData *ked, case ALE_GROUP: /* action group */ return agrp_keyframes_loop(ked, (bActionGroup *)data, key_ok, key_cb, fcu_cb); case ALE_ACTION_LAYERED: + case ALE_ACTION_BINDING: /* This function is only used in nlaedit_apply_scale_exec(). Since the NLA has no support for - * Animation data-blocks in strips, there is no need to implement this here. */ + * layered Actions in strips, there is no need to implement this here. */ return 0; case ALE_ACT: /* action */ - return act_keyframes_loop(ked, (bAction *)data, key_ok, key_cb, fcu_cb); + return action_legacy_keyframes_loop(ked, (bAction *)data, key_ok, key_cb, fcu_cb); case ALE_OB: /* object */ return ob_keyframes_loop(ked, ads, (Object *)data, key_ok, key_cb, fcu_cb); case ALE_SCE: /* scene */ diff --git a/source/blender/editors/animation/keyframes_keylist.cc b/source/blender/editors/animation/keyframes_keylist.cc index 6f721b47283..303bc929eed 100644 --- a/source/blender/editors/animation/keyframes_keylist.cc +++ b/source/blender/editors/animation/keyframes_keylist.cc @@ -38,6 +38,8 @@ #include "ANIM_action.hh" +using namespace blender; + /* *************************** Keyframe Processing *************************** */ /* ActKeyColumns (Keyframe Columns) ------------------------------------------ */ @@ -1159,6 +1161,19 @@ void action_group_to_keylist(AnimData *adt, } } +void action_binding_to_keylist(AnimData *adt, + animrig::Action &action, + const animrig::binding_handle_t binding_handle, + AnimKeylist *keylist, + const int saction_flag, + blender::float2 range) +{ + BLI_assert(GS(action.id.name) == ID_AC); + for (FCurve *fcurve : fcurves_for_animation(action, binding_handle)) { + fcurve_to_keylist(adt, fcurve, keylist, saction_flag, range); + } +} + void action_to_keylist(AnimData *adt, bAction *dna_action, AnimKeylist *keylist, @@ -1183,9 +1198,7 @@ void action_to_keylist(AnimData *adt, * Assumption: the animation is bound to adt->binding_handle. This assumption will break when we * have things like reference strips, where the strip can reference another binding handle. */ - for (FCurve *fcurve : fcurves_for_animation(action, adt->binding_handle)) { - fcurve_to_keylist(adt, fcurve, keylist, saction_flag, range); - } + action_binding_to_keylist(adt, action, adt->binding_handle, keylist, saction_flag, range); } void gpencil_to_keylist(bDopeSheet *ads, bGPdata *gpd, AnimKeylist *keylist, const bool active) diff --git a/source/blender/editors/include/ED_anim_api.hh b/source/blender/editors/include/ED_anim_api.hh index 978ff8a9a49..3b56677ef63 100644 --- a/source/blender/editors/include/ED_anim_api.hh +++ b/source/blender/editors/include/ED_anim_api.hh @@ -133,8 +133,9 @@ enum eAnimCont_Types { /** * Some types for easier type-testing * - * \note need to keep the order of these synchronized with the channels define code - * which is used for drawing and handling channel lists for. + * \note need to keep the order of these synchronized with the channels define code (ACF_XXX must + * have the same value as ANIMTYPE_XXX below) which is used for drawing and handling channel lists + * for. */ enum eAnim_ChannelType { ANIMTYPE_NONE = 0, @@ -152,7 +153,8 @@ enum eAnim_ChannelType { ANIMTYPE_NLACURVE, ANIMTYPE_FILLACT_LAYERED, /* Layered Actions. */ - ANIMTYPE_FILLACTD, /* Legacy Actions. */ + ANIMTYPE_ACTION_BINDING, + ANIMTYPE_FILLACTD, /* Legacy Actions. */ ANIMTYPE_FILLDRIVERS, ANIMTYPE_DSMAT, @@ -212,6 +214,7 @@ enum eAnim_KeyType { ALE_ACT, /* Action summary (legacy). */ ALE_GROUP, /* Action Group summary (legacy). */ ALE_ACTION_LAYERED, /* Action summary (layered). */ + ALE_ACTION_BINDING, /* Action binding summary. */ ALE_GREASE_PENCIL_CEL, /* Grease Pencil Cels. */ ALE_GREASE_PENCIL_DATA, /* Grease Pencil Cels summary. */ @@ -252,6 +255,18 @@ struct bAnimListElem { int flag; /** for un-named data, the index of the data in its collection */ int index; + /** + * For data that is owned by a specific binding, its handle. + * + * This is not declared as blender::animrig::binding_handle_t to avoid all the users of this + * header file to get the animrig module as extra dependency (which would spread to the undo + * system, lineart, etc). It's probably best to split off this struct definition from the rest of + * this header, as most code that uses this header doesn't need to know the definition of this + * struct. + * + * TODO: split off into separate header file. + */ + int32_t binding_handle; /** Tag the element for updating. */ eAnim_Update_Flags update; @@ -278,6 +293,8 @@ struct bAnimListElem { ID *id; /** source of the animation data attached to ID block */ AnimData *adt; + /** Main containing the ID. */ + Main *bmain; /** * For list element which corresponds to a f-curve, this is an ID which @@ -881,6 +898,15 @@ bool ANIM_fmodifiers_paste_from_buf(ListBase *modifiers, bool replace, FCurve *c */ int getname_anim_fcurve(char *name, ID *id, FCurve *fcu); +/** + * Get the name of an F-Curve that's animating a specific binding. + * + * This function iterates the Binding's users to find an ID that allows it to resolve its RNA path. + */ +std::string getname_anim_fcurve_bound(Main &bmain, + const blender::animrig::Binding &binding, + FCurve &fcurve); + /** * Automatically determine a color for the nth F-Curve. */ diff --git a/source/blender/editors/include/ED_keyframes_draw.hh b/source/blender/editors/include/ED_keyframes_draw.hh index e98f1647430..d04f449481b 100644 --- a/source/blender/editors/include/ED_keyframes_draw.hh +++ b/source/blender/editors/include/ED_keyframes_draw.hh @@ -79,6 +79,14 @@ void ED_add_action_layered_channel(ChannelDrawList *channel_list, const float ypos, const float yscale_fac, int saction_flag); +/* Action Binding summary. */ +void ED_add_action_binding_channel(ChannelDrawList *channel_list, + AnimData *adt, + blender::animrig::Action &action, + blender::animrig::Binding &binding, + float ypos, + float yscale_fac, + int saction_flag); /* Legacy Action Summary */ void ED_add_action_channel(ChannelDrawList *draw_list, AnimData *adt, diff --git a/source/blender/editors/include/ED_keyframes_keylist.hh b/source/blender/editors/include/ED_keyframes_keylist.hh index 80ea0badfe7..da0656ac0f9 100644 --- a/source/blender/editors/include/ED_keyframes_keylist.hh +++ b/source/blender/editors/include/ED_keyframes_keylist.hh @@ -13,6 +13,8 @@ #include "DNA_curve_types.h" +#include "ANIM_action.hh" + struct AnimData; struct CacheFile; struct FCurve; @@ -30,6 +32,11 @@ struct bDopeSheet; struct bGPDlayer; struct bGPdata; +namespace blender::animrig { +class Action; +class Binding; +} // namespace blender::animrig + /* ****************************** Base Structs ****************************** */ struct AnimKeylist; @@ -164,6 +171,12 @@ void action_group_to_keylist(AnimData *adt, /* Action */ void action_to_keylist( AnimData *adt, bAction *act, AnimKeylist *keylist, int saction_flag, blender::float2 range); +void action_binding_to_keylist(AnimData *adt, + blender::animrig::Action &action, + blender::animrig::binding_handle_t binding_handle, + AnimKeylist *keylist, + int saction_flag, + blender::float2 range); /* Object */ void ob_to_keylist( bDopeSheet *ads, Object *ob, AnimKeylist *keylist, int saction_flag, blender::float2 range); diff --git a/source/blender/editors/space_action/action_draw.cc b/source/blender/editors/space_action/action_draw.cc index 0441d12bd9c..85f43ae51d1 100644 --- a/source/blender/editors/space_action/action_draw.cc +++ b/source/blender/editors/space_action/action_draw.cc @@ -28,6 +28,8 @@ #include "BKE_bake_geometry_nodes_modifier.hh" #include "BKE_pointcache.h" +#include "ANIM_action.hh" + /* Everything from source (BIF, BDR, BSE) ------------------------------ */ #include "GPU_immediate.hh" @@ -45,6 +47,8 @@ #include "action_intern.hh" +using namespace blender; + /* -------------------------------------------------------------------- */ /** \name Channel List * \{ */ @@ -376,6 +380,15 @@ static void draw_keyframes(bAnimContext *ac, scale_factor, action_flag); break; + case ALE_ACTION_BINDING: + ED_add_action_binding_channel(draw_list, + adt, + static_cast(ale->key_data)->wrap(), + *static_cast(ale->data), + ycenter, + scale_factor, + action_flag); + break; case ALE_ACT: ED_add_action_channel(draw_list, adt, diff --git a/source/blender/editors/space_action/action_select.cc b/source/blender/editors/space_action/action_select.cc index 612033a82dd..f556f30ead4 100644 --- a/source/blender/editors/space_action/action_select.cc +++ b/source/blender/editors/space_action/action_select.cc @@ -51,6 +51,8 @@ #include "action_intern.hh" +using namespace blender; + /* -------------------------------------------------------------------- */ /** \name Keyframes Stuff * \{ */ @@ -117,6 +119,14 @@ static void actkeys_list_element_to_keylist(bAnimContext *ac, action_to_keylist(adt, action, keylist, 0, range); break; } + case ALE_ACTION_BINDING: { + animrig::Action *action = static_cast(ale->key_data); + animrig::Binding *binding = static_cast(ale->data); + BLI_assert(action); + BLI_assert(binding); + action_binding_to_keylist(adt, *action, binding->handle, keylist, 0, range); + break; + } case ALE_ACT: { bAction *act = (bAction *)ale->key_data; action_to_keylist(adt, act, keylist, 0, range); diff --git a/source/blender/editors/space_nla/nla_buttons.cc b/source/blender/editors/space_nla/nla_buttons.cc index 8017de92789..81c42e827e0 100644 --- a/source/blender/editors/space_nla/nla_buttons.cc +++ b/source/blender/editors/space_nla/nla_buttons.cc @@ -170,6 +170,7 @@ bool nla_panel_context(const bContext *C, case ANIMTYPE_NLACONTROLS: case ANIMTYPE_NLACURVE: case ANIMTYPE_FILLACT_LAYERED: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_FILLACTD: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSMCLIP: diff --git a/source/blender/editors/space_nla/nla_draw.cc b/source/blender/editors/space_nla/nla_draw.cc index 7645265d9bc..85ec2bdcf3d 100644 --- a/source/blender/editors/space_nla/nla_draw.cc +++ b/source/blender/editors/space_nla/nla_draw.cc @@ -917,6 +917,7 @@ void draw_nla_main_data(bAnimContext *ac, SpaceNla *snla, ARegion *region) case ANIMTYPE_NLACONTROLS: case ANIMTYPE_NLACURVE: case ANIMTYPE_FILLACT_LAYERED: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_FILLACTD: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSMAT: diff --git a/source/blender/editors/space_nla/nla_tracks.cc b/source/blender/editors/space_nla/nla_tracks.cc index 8dfec3e33ec..1ddabc8f4a8 100644 --- a/source/blender/editors/space_nla/nla_tracks.cc +++ b/source/blender/editors/space_nla/nla_tracks.cc @@ -264,7 +264,8 @@ static int mouse_nla_tracks(bContext *C, bAnimContext *ac, int track_index, shor break; } case ANIMTYPE_FILLACT_LAYERED: - /* The NLA doesn't support Animation data-blocks. */ + case ANIMTYPE_ACTION_BINDING: + /* The NLA doesn't support layered Actions. */ break; default: if (G.debug & G_DEBUG) { diff --git a/source/blender/editors/transform/transform_convert_action.cc b/source/blender/editors/transform/transform_convert_action.cc index 955199f8284..a63214fb48e 100644 --- a/source/blender/editors/transform/transform_convert_action.cc +++ b/source/blender/editors/transform/transform_convert_action.cc @@ -680,6 +680,7 @@ static void createTransActionData(bContext *C, TransInfo *t) case ANIMTYPE_GROUP: case ANIMTYPE_NLACONTROLS: case ANIMTYPE_FILLACT_LAYERED: + case ANIMTYPE_ACTION_BINDING: case ANIMTYPE_FILLACTD: case ANIMTYPE_FILLDRIVERS: case ANIMTYPE_DSMAT: diff --git a/source/blender/makesdna/DNA_action_types.h b/source/blender/makesdna/DNA_action_types.h index 67f8028b18b..816d8eca695 100644 --- a/source/blender/makesdna/DNA_action_types.h +++ b/source/blender/makesdna/DNA_action_types.h @@ -853,6 +853,12 @@ typedef enum eDopeSheet_FilterFlag { /** for 'DopeSheet' Editors - include 'summary' line */ ADS_FILTER_SUMMARY = (1 << 4), + /** + * Show all Action bindings; if not set, only show the Binding of the + * data-block that's being animated by the Action. + */ + ADS_FILTER_ALL_BINDINGS = (1 << 5), + /* datatype-based filtering */ ADS_FILTER_NOSHAPEKEYS = (1 << 6), ADS_FILTER_NOMESH = (1 << 7), @@ -940,8 +946,11 @@ typedef struct SpaceAction { /** Copied to region. */ View2D v2d DNA_DEPRECATED; - /** The currently active action. */ + /** The currently active action and its binding. */ bAction *action; + int32_t action_binding_handle; + char _pad2[4]; + /** The currently active context (when not showing action). */ bDopeSheet ads; @@ -1142,8 +1151,11 @@ typedef struct ActionBinding { */ int32_t handle; + /** \see #blender::animrig::Binding::flags() */ + int8_t binding_flags; + uint8_t _pad1[3]; + /** Runtime data. Set to nullptr when writing to disk. */ - uint8_t _pad1[4]; ActionBindingRuntimeHandle *runtime; #ifdef __cplusplus @@ -1222,4 +1234,6 @@ static_assert( std::is_same_v); static_assert( std::is_same_v); +static_assert( + std::is_same_v); #endif diff --git a/source/blender/makesrna/intern/rna_action.cc b/source/blender/makesrna/intern/rna_action.cc index e6a79dc8153..c7a0d4e5c25 100644 --- a/source/blender/makesrna/intern/rna_action.cc +++ b/source/blender/makesrna/intern/rna_action.cc @@ -877,6 +877,12 @@ static void rna_def_dopesheet(BlenderRNA *brna) RNA_def_property_ui_icon(prop, ICON_RESTRICT_SELECT_OFF, 0); RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr); + prop = RNA_def_property(srna, "show_all_bindings", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "filterflag", ADS_FILTER_ALL_BINDINGS); + RNA_def_property_ui_text(prop, "Show All Bindings", "Show all the Action's Bindings"); + RNA_def_property_ui_icon(prop, ICON_LINKED, 0); /* TODO: select icon for Bindings. */ + RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr); + prop = RNA_def_property(srna, "show_hidden", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_sdna(prop, nullptr, "filterflag", ADS_FILTER_INCL_HIDDEN); RNA_def_property_ui_text(