diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 79b7db15cc7..8a7563f92b7 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4839,7 +4839,7 @@ def km_pose(params): op_menu("VIEW3D_MT_bone_options_enable", {"type": 'W', "value": 'PRESS', "shift": True, "ctrl": True}), op_menu("VIEW3D_MT_bone_options_disable", {"type": 'W', "value": 'PRESS', "alt": True}), ("armature.collection_show_all", {"type": 'ACCENT_GRAVE', "value": 'PRESS', "ctrl": True}, None), - op_menu("VIEW3D_MT_bone_collections", {"type": 'M', "value": 'PRESS', "shift": True}), + ("armature.assign_to_collection", {"type": 'M', "value": 'PRESS', "shift": True}, None), ("armature.move_to_collection", {"type": 'M', "value": 'PRESS'}, None), ("transform.bbone_resize", {"type": 'S', "value": 'PRESS', "shift": True, "ctrl": True, "alt": True}, None), ("anim.keyframe_insert", {"type": 'I', "value": 'PRESS'}, None), @@ -5779,7 +5779,7 @@ def km_edit_armature(params): op_menu("VIEW3D_MT_bone_options_disable", {"type": 'W', "value": 'PRESS', "alt": True}), # Armature/bone layers. ("armature.collection_show_all", {"type": 'ACCENT_GRAVE', "value": 'PRESS', "ctrl": True}, None), - op_menu("VIEW3D_MT_bone_collections", {"type": 'M', "value": 'PRESS', "shift": True}), + ("armature.assign_to_collection", {"type": 'M', "value": 'PRESS', "shift": True}, None), ("armature.move_to_collection", {"type": 'M', "value": 'PRESS'}, None), # Special transforms. op_tool_optional( diff --git a/scripts/startup/bl_ui/properties_data_armature.py b/scripts/startup/bl_ui/properties_data_armature.py index 547bb8c6a26..9ff22cf778f 100644 --- a/scripts/startup/bl_ui/properties_data_armature.py +++ b/scripts/startup/bl_ui/properties_data_armature.py @@ -113,20 +113,7 @@ class DATA_PT_bone_collections(ArmatureButtonsPanel, Panel): active_bcoll = arm.collections.active row = layout.row() - - rows = 1 - if active_bcoll: - rows = 4 - - row.template_list( - "DATA_UL_bone_collections", - "collections", - arm, - "collections", - arm.collections, - "active_index", - rows=rows, - ) + row.template_bone_collection_tree() col = row.column(align=True) col.operator("armature.collection_add", icon='ADD', text="") @@ -164,6 +151,35 @@ class ARMATURE_MT_collection_context_menu(Menu): layout.operator("armature.collection_show_all") +class ARMATURE_MT_collection_tree_context_menu(Menu): + bl_label = "Bone Collections" + + def draw(self, context): + layout = self.layout + arm = context.armature + + props = layout.operator( + "armature.collection_add", text="Add Child Collection" + ) + props.parent_index = arm.collections.active_index + layout.operator("armature.collection_remove") + + layout.separator() + + layout.operator("armature.collection_solo_visibility") + layout.operator("armature.collection_show_all") + + layout.separator() + + layout.operator("armature.collection_assign", text="Assign Selected Bones") + layout.operator("armature.collection_unassign", text="Remove Selected Bones") + + layout.separator() + + layout.operator("armature.collection_select", text="Select Bones") + layout.operator("armature.collection_deselect", text="Deselect Bones") + + class DATA_PT_iksolver_itasc(ArmatureButtonsPanel, Panel): bl_label = "Inverse Kinematics" bl_options = {'DEFAULT_CLOSED'} @@ -291,6 +307,7 @@ classes = ( DATA_PT_bone_collections, DATA_UL_bone_collections, ARMATURE_MT_collection_context_menu, + ARMATURE_MT_collection_tree_context_menu, DATA_PT_motion_paths, DATA_PT_motion_paths_display, DATA_PT_display, diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 2d9b5056f5a..19cbb5b881f 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -3984,7 +3984,6 @@ class VIEW3D_MT_pose(Menu): layout.separator() layout.menu("VIEW3D_MT_pose_motion") - layout.operator("armature.move_to_collection", text="Move to Bone Collection") layout.menu("VIEW3D_MT_bone_collections") layout.separator() @@ -4078,46 +4077,12 @@ class VIEW3D_MT_bone_collections(Menu): def draw(self, context): layout = self.layout - if not context.selected_pose_bones and not context.selected_bones: - # If there are no bones to assign to any collection, there's no need - # to go over all the bone collections & try to build up the menu. - # - # The poll function shouldn't test for this, because returning False - # there will hide this menu completely from the Pose menu, and - # that's going too far. - layout.enabled = False - layout.label(text="- select bones to operate on first -") - return + layout.operator("armature.move_to_collection") + layout.operator("armature.assign_to_collection") + + layout.separator() layout.operator("armature.collection_show_all") - layout.separator() - - arm = context.object.data - bone = context.active_bone - - found_editable_bcoll = False - for bcoll in arm.collections: - if not bcoll.is_editable: - continue - found_editable_bcoll = True - - if bcoll.name in bone.collections: - props = layout.operator("armature.collection_unassign", - text=bcoll.name, - icon='REMOVE') - else: - props = layout.operator("armature.collection_assign", - text=bcoll.name, - icon='ADD') - props.name = bcoll.name - - if arm.collections and not found_editable_bcoll: - row = layout.row() - row.enabled = False - row.label(text="All bone collections are read-only") - - layout.separator() - props = layout.operator("armature.collection_create_and_assign", text="Assign to New Collection") props.name = "New Collection" diff --git a/source/blender/animrig/ANIM_bone_collections.hh b/source/blender/animrig/ANIM_bone_collections.hh index fa4541a7d17..7995cd8f7f1 100644 --- a/source/blender/animrig/ANIM_bone_collections.hh +++ b/source/blender/animrig/ANIM_bone_collections.hh @@ -68,9 +68,14 @@ void ANIM_armature_runtime_free(struct bArmature *armature); /** * Add a new bone collection to the given armature. * + * \param parent_index Index into the Armature's `collections_array`. -1 adds it + * as a root (i.e. parentless) collection. + * * The Armature owns the returned pointer. */ -BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name); +BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, + const char *name, + int parent_index = -1); /** * Add a bone collection to the Armature. @@ -81,10 +86,10 @@ BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name * NOTE: this should not typically be used. It is only used by the library overrides system to * apply override operations. */ -struct BoneCollection *ANIM_armature_bonecoll_insert_copy_after( - struct bArmature *armature, - struct BoneCollection *anchor, - const struct BoneCollection *bcoll_to_copy); +BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature_dst, + const bArmature *armature_src, + const BoneCollection *anchor_in_dst, + const BoneCollection *bcoll_to_copy); /** * Remove the bone collection at `index` from the armature. @@ -153,6 +158,10 @@ bool ANIM_armature_bonecoll_is_editable(const struct bArmature *armature, * \return true if the collection was successfully moved, false otherwise. * The latter happens if either index is out of bounds, or if the indices * are equal. + * + * \note This function is limited to moving between siblings of the bone + * collection at `from_index`. + * * \note This function ensures that the element at index `from_index` (before * the call) will end up at `to_index` (after the call). The element at * `to_index` before the call will shift towards `from_index`; in other words, @@ -160,13 +169,32 @@ bool ANIM_armature_bonecoll_is_editable(const struct bArmature *armature, * before or after that one. * * TODO: add ASCII-art illustration of left & right movement. + * + * \see blender::animrig::armature_bonecoll_move_to_parent() to move bone + * collections between different parents. */ bool ANIM_armature_bonecoll_move_to_index(bArmature *armature, int from_index, int to_index); +enum class MoveLocation { + Before, /* Move to before the item at the given index. */ + After, /* Move to after the item at the given index. */ +}; + +int ANIM_armature_bonecoll_move_before_after_index(bArmature *armature, + int from_index, + int to_index, + MoveLocation before_after); + /** * Move the bone collection by \a step places up/down. * * \return whether the move actually happened. + * + * \note This function is limited to moving between siblings of the bone + * collection at `from_index`. + * + * \see blender::animrig::armature_bonecoll_move_to_parent() to move bone + * collections between different parents. */ bool ANIM_armature_bonecoll_move(struct bArmature *armature, struct BoneCollection *bcoll, @@ -175,6 +203,13 @@ bool ANIM_armature_bonecoll_move(struct bArmature *armature, struct BoneCollection *ANIM_armature_bonecoll_get_by_name( struct bArmature *armature, const char *name) ATTR_WARN_UNUSED_RESULT; +/** Scan the bone collections to find the one with the given name. + * + * \return the index of the bone collection, or -1 if not found. + */ +int ANIM_armature_bonecoll_get_index_by_name(struct bArmature *armature, + const char *name) ATTR_WARN_UNUSED_RESULT; + void ANIM_armature_bonecoll_name_set(struct bArmature *armature, struct BoneCollection *bcoll, const char *name); @@ -205,6 +240,12 @@ void ANIM_armature_bonecoll_unassign_all_editbone(struct EditBone *ebone); void ANIM_armature_bonecoll_assign_active(const struct bArmature *armature, struct EditBone *ebone); +/** + * Return whether the Armature's active bone is assigned to the given bone collection. + */ +bool ANIM_armature_bonecoll_contains_active_bone(const struct bArmature *armature, + const struct BoneCollection *bcoll); + /** * Reconstruct the bone collection memberships, based on the bone runtime data. * @@ -270,6 +311,42 @@ namespace blender::animrig { */ int armature_bonecoll_find_index(const bArmature *armature, const ::BoneCollection *bcoll); +/** + * Return the index of the given bone collection's parent, or -1 if it has no parent. + */ +int armature_bonecoll_find_parent_index(const bArmature *armature, int bcoll_index); + +bool armature_bonecoll_is_root(const bArmature *armature, int bcoll_index); + +bool armature_bonecoll_is_child_of(const bArmature *armature, + int potential_parent_index, + int potential_child_index); + +bool armature_bonecoll_is_decendent_of(const bArmature *armature, + int potential_parent_index, + int potential_decendent_index); + +bool bonecoll_has_children(const BoneCollection *bcoll); + +/** + * Move a bone collection from one parent to another. + * + * \param from_bcoll_index index of the bone collection to move. + * \param to_child_num gap index of where to insert the collection; 0 to make it + * the first child, and parent->child_count to make it the last child. -1 also + * works as an indicator for the last child, as that makes it possible to call + * this function without requiring the caller to find the BoneCollection* of the + * parent. + * \param from_parent_index index of its current parent (-1 if it is a root collection). + * \param to_parent_index index of the new parent (-1 if it is to become a root collection). + * \return the collection's new index in the collections_array. + */ +int armature_bonecoll_move_to_parent(bArmature *armature, + int from_bcoll_index, + int to_child_num, + int from_parent_index, + int to_parent_index); + /* -------------------------------------------------------------------- * The following functions are only used by edit-mode Armature undo: */ diff --git a/source/blender/animrig/CMakeLists.txt b/source/blender/animrig/CMakeLists.txt index d3afa4a55d0..dff06f030b8 100644 --- a/source/blender/animrig/CMakeLists.txt +++ b/source/blender/animrig/CMakeLists.txt @@ -38,6 +38,7 @@ set(SRC ANIM_keyframing.hh ANIM_rna.hh ANIM_visualkey.hh + intern/bone_collections_internal.hh ) set(LIB diff --git a/source/blender/animrig/intern/bone_collections.cc b/source/blender/animrig/intern/bone_collections.cc index 4a7b822e079..4baeb95391b 100644 --- a/source/blender/animrig/intern/bone_collections.cc +++ b/source/blender/animrig/intern/bone_collections.cc @@ -31,6 +31,8 @@ #include "ANIM_armature_iter.hh" #include "ANIM_bone_collections.hh" +#include "intern/bone_collections_internal.hh" + #include #include @@ -44,6 +46,7 @@ namespace { constexpr eBoneCollection_Flag default_flags = BONE_COLLECTION_VISIBLE | BONE_COLLECTION_SELECTABLE; constexpr auto bonecoll_default_name = "Bones"; + } // namespace BoneCollection *ANIM_bonecoll_new(const char *name) @@ -155,31 +158,55 @@ static void bonecoll_insert_at_index(bArmature *armature, BoneCollection *bcoll, sizeof(BoneCollection *) * (armature->collection_array_num + 1), __func__); - /* Shift over the collections to make room at the given index. */ - if (index < armature->collection_array_num) { - BoneCollection **start = armature->collection_array + index; - const size_t count = armature->collection_array_num - index; - memmove((void *)(start + 1), (void *)start, count * sizeof(BoneCollection *)); - } - - armature->collection_array[index] = bcoll; + /* To keep the memory consistent, insert the new element at the end of the + * now-grown array, then rotate it into place. */ + armature->collection_array[armature->collection_array_num] = bcoll; armature->collection_array_num++; + const int rotate_count = armature->collection_array_num - index - 1; + internal::bonecolls_rotate_block(armature, index, rotate_count, +1); + if (armature->runtime.active_collection_index >= index) { ANIM_armature_bonecoll_active_index_set(armature, armature->runtime.active_collection_index + 1); } } -/** - * Appends bcoll to the end of armature's array of bone collections. - */ -static void bonecoll_append(bArmature *armature, BoneCollection *bcoll) +static void bonecoll_insert_as_root(bArmature *armature, BoneCollection *bcoll, int at_index) { - bonecoll_insert_at_index(armature, bcoll, armature->collection_array_num); + BLI_assert(at_index >= -1); + BLI_assert(at_index <= armature->collection_root_count); + if (at_index < 0) { + at_index = armature->collection_root_count; + } + + bonecoll_insert_at_index(armature, bcoll, at_index); + armature->collection_root_count++; } -BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name) +static int bonecoll_insert_as_child(bArmature *armature, + BoneCollection *bcoll, + const int parent_index) +{ + BLI_assert_msg(parent_index >= 0, "Armature bone collection index should be 0 or larger"); + BLI_assert_msg(parent_index < armature->collection_array_num, + "Parent bone collection index should not point beyond the end of the array"); + + BoneCollection *parent = armature->collection_array[parent_index]; + if (parent->child_index == 0) { + /* This parent doesn't have any children yet, so place them at the end of the array. */ + parent->child_index = armature->collection_array_num; + } + const int insert_at_index = parent->child_index + parent->child_count; + bonecoll_insert_at_index(armature, bcoll, insert_at_index); + parent->child_count++; + + return insert_at_index; +} + +BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, + const char *name, + const int parent_index) { BoneCollection *bcoll = ANIM_bonecoll_new(name); @@ -189,38 +216,165 @@ BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name } bonecoll_ensure_name_unique(armature, bcoll); - bonecoll_append(armature, bcoll); + + if (parent_index < 0) { + bonecoll_insert_as_root(armature, bcoll, -1); + } + else { + bonecoll_insert_as_child(armature, bcoll, parent_index); + } return bcoll; } -BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature, - BoneCollection *anchor, - const BoneCollection *bcoll_to_copy) +/** + * Copy a BoneCollection to a new armature, updating its internal pointers to + * point to the new armature. + * + * This *only* updates the cloned BoneCollection, and does *not* actually add it + * to the armature. + * + * Child collections are not taken into account; the returned bone collection is + * without children, regardless of `bcoll_to_copy`. + */ +static BoneCollection *copy_and_update_ownership(const bArmature *armature_dst, + const BoneCollection *bcoll_to_copy) { BoneCollection *bcoll = static_cast(MEM_dupallocN(bcoll_to_copy)); - /* Remap the bone pointers to the given armature, as `bcoll_to_copy` will - * likely be owned by another copy of the armature. */ - BLI_duplicatelist(&bcoll->bones, &bcoll->bones); - BLI_assert_msg(armature->bonehash, "Expected armature bone hash to be there"); - LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) { - member->bone = BKE_armature_find_bone_name(armature, member->bone->name); - } + /* Reset the child_index and child_count properties. These are unreliable when + * coming from an override, as the original array might have been completely + * reshuffled. Children will have to be copied separately. */ + bcoll->child_index = 0; + bcoll->child_count = 0; - if (bcoll_to_copy->prop) { + if (bcoll->prop) { bcoll->prop = IDP_CopyProperty_ex(bcoll_to_copy->prop, 0 /*do_id_user ? 0 : LIB_ID_CREATE_NO_USER_REFCOUNT*/); } - const int anchor_index = armature_bonecoll_find_index(armature, anchor); - bonecoll_insert_at_index(armature, bcoll, anchor_index + 1); - bonecoll_ensure_name_unique(armature, bcoll); + /* Remap the bone pointers to the given armature, as `bcoll_to_copy` is + * assumed to be owned by another armature. */ + BLI_duplicatelist(&bcoll->bones, &bcoll->bones); + BLI_assert_msg(armature_dst->bonehash, "Expected armature bone hash to be there"); + LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) { + member->bone = BKE_armature_find_bone_name(const_cast(armature_dst), + member->bone->name); + } + + /* Now that the collection points to the right bones, these bones can be + * updated to point to this collection. */ add_reverse_pointers(bcoll); return bcoll; } +/** + * Copy all the child collections of the specified parent, from `armature_src` to `armature_dst`. + * + * This assumes that the parent itself has already been copied. + */ +static void liboverride_recursively_add_children(bArmature *armature_dst, + const bArmature *armature_src, + const int parent_bcoll_dst_index, + const BoneCollection *parent_bcoll_src) +{ + BLI_assert_msg(parent_bcoll_dst_index >= 0, + "this function can only add children to another collection, it cannot add roots"); + + /* Iterate over the children in `armature_src`, and clone them one by one into `armature_dst`. + * + * This uses two loops. The first one adds all the children, the second loop iterates over those + * children for the recursion step. As this performs a "breadth-first insertion", it requires + * considerably less shuffling of the array as when the recursion was done immediately after + * inserting a child. */ + BoneCollection *parent_bcoll_dst = armature_dst->collection_array[parent_bcoll_dst_index]; + + /* Big Fat Assumption: because this code runs as part of the library override system, it is + * assumed that the parent is either a newly added root, or another child that was also added by + * the liboverride system. Because this would never add a child to an original "sequence of + * siblings", insertions of children always happen at the end of the array. This means that + * `parent_bcoll_dst_index` remains constant during this entire function. */ + + /* Copy & insert all the children. */ + for (int bcoll_src_index = parent_bcoll_src->child_index; + bcoll_src_index < parent_bcoll_src->child_index + parent_bcoll_src->child_count; + bcoll_src_index++) + { + const BoneCollection *bcoll_src = armature_src->collection_array[bcoll_src_index]; + BoneCollection *bcoll_dst = copy_and_update_ownership(armature_dst, bcoll_src); + + const int bcoll_index_dst = bonecoll_insert_as_child( + armature_dst, bcoll_dst, parent_bcoll_dst_index); + +#ifndef NDEBUG + /* Check that the above Big Fat Assumption holds. */ + BLI_assert_msg(bcoll_index_dst > parent_bcoll_dst_index, + "expecting children to be added to the array AFTER their parent"); +#else + (void)bcoll_index_dst; +#endif + + bonecoll_ensure_name_unique(armature_dst, bcoll_dst); + } + + /* Double-check that the above Big Fat Assumption holds. */ +#ifndef NDEBUG + const int new_parent_bcoll_dst_index = armature_bonecoll_find_index(armature_dst, + parent_bcoll_dst); + BLI_assert_msg(new_parent_bcoll_dst_index == parent_bcoll_dst_index, + "did not expect parent_bcoll_dst_index to change"); +#endif + + /* Recurse into the children to copy grandchildren. */ + BLI_assert_msg(parent_bcoll_dst->child_count == parent_bcoll_src->child_count, + "all children should have been copied"); + for (int child_num = 0; child_num < parent_bcoll_dst->child_count; child_num++) { + const int bcoll_src_index = parent_bcoll_src->child_index + child_num; + const int bcoll_dst_index = parent_bcoll_dst->child_index + child_num; + + const BoneCollection *bcoll_src = armature_src->collection_array[bcoll_src_index]; + liboverride_recursively_add_children(armature_dst, armature_src, bcoll_dst_index, bcoll_src); + } +} + +BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature_dst, + const bArmature *armature_src, + const BoneCollection *anchor_in_dst, + const BoneCollection *bcoll_to_copy) +{ +#ifndef NDEBUG + /* Check that this bone collection is really a root, as this is assumed by the + * rest of this function. This is an O(n) check, though, so that's why it's + * only running in debug builds. */ + const int bcoll_index_src = armature_bonecoll_find_index(armature_src, bcoll_to_copy); + if (!armature_bonecoll_is_root(armature_src, bcoll_index_src)) { + printf( + "Armature \"%s\" has library override operation that adds non-root bone collection " + "\"%s\". This is unexpected, please file a bug report.\n", + armature_src->id.name + 2, + bcoll_to_copy->name); + } +#endif + + BoneCollection *bcoll = copy_and_update_ownership(armature_dst, bcoll_to_copy); + + const int anchor_index = armature_bonecoll_find_index(armature_dst, anchor_in_dst); + const int bcoll_index = anchor_index + 1; + BLI_assert_msg( + bcoll_index <= armature_dst->collection_root_count, + "did not expect library override to add a child bone collection, only roots are expected"); + bonecoll_insert_as_root(armature_dst, bcoll, bcoll_index); + bonecoll_ensure_name_unique(armature_dst, bcoll); + + /* Library override operations are only constructed for the root bones. This means that handling + * this operation should also include copying the children. */ + liboverride_recursively_add_children(armature_dst, armature_src, bcoll_index, bcoll_to_copy); + + ANIM_armature_bonecoll_active_runtime_refresh(armature_dst); + return bcoll; +} + static void armature_bonecoll_active_clear(bArmature *armature) { armature->runtime.active_collection_index = -1; @@ -318,39 +472,88 @@ bool ANIM_armature_bonecoll_move_to_index(bArmature *armature, return false; } - BoneCollection *bcoll = armature->collection_array[from_index]; - - /* Shift collections over to fill the gap at from_index and make room at to_index. */ - if (from_index < to_index) { - BoneCollection **start = armature->collection_array + from_index + 1; - const size_t count = to_index - from_index; - memmove((void *)(start - 1), (void *)start, count * sizeof(BoneCollection *)); - } - else { - BoneCollection **start = armature->collection_array + to_index; - const size_t count = from_index - to_index; - memmove((void *)(start + 1), (void *)start, count * sizeof(BoneCollection *)); + /* Only allow moving within the same parent. This is written a bit awkwardly to avoid two calls + * to `armature_bonecoll_find_parent_index()` as that is O(n) in the number of bone collections. + */ + const int parent_index = armature_bonecoll_find_parent_index(armature, from_index); + if (!armature_bonecoll_is_child_of(armature, parent_index, to_index)) { + return false; } - armature->collection_array[to_index] = bcoll; + if (parent_index < 0) { + /* Roots can just be moved around, as there is no `child_index` to update in this case. */ + internal::bonecolls_move_to_index(armature, from_index, to_index); + return true; + } - /* Adjust the active collection index. */ - if (from_index == armature->runtime.active_collection_index) { - /* If the active collection is the collection we moved. */ - ANIM_armature_bonecoll_active_index_set(armature, to_index); - } - else if (from_index <= armature->runtime.active_collection_index && - armature->runtime.active_collection_index <= to_index) - { - /* If the active collection is within the span of shifted collections. */ - const int offset = from_index < to_index ? -1 : 1; - ANIM_armature_bonecoll_active_index_set(armature, - armature->runtime.active_collection_index + offset); - } + /* Store the parent's child_index, as that might move if to_index is the first child + * (bonecolls_move_to_index() will keep it pointing at that first child). */ + BoneCollection *parent_bcoll = armature->collection_array[parent_index]; + const int old_parent_child_index = parent_bcoll->child_index; + + internal::bonecolls_move_to_index(armature, from_index, to_index); + + parent_bcoll->child_index = old_parent_child_index; return true; } +static int bonecoll_child_number(const bArmature *armature, + const int parent_bcoll_index, + const int bcoll_index) +{ + if (parent_bcoll_index < 0) { + /* Root bone collections are always at the start of the array, and thus their index is the + * 'child number'. */ + return bcoll_index; + } + + const BoneCollection *parent_bcoll = armature->collection_array[parent_bcoll_index]; + return bcoll_index - parent_bcoll->child_index; +} + +int ANIM_armature_bonecoll_move_before_after_index(bArmature *armature, + const int from_index, + int to_index, + const MoveLocation before_after) +{ + const int from_parent_index = armature_bonecoll_find_parent_index(armature, from_index); + const int to_parent_index = armature_bonecoll_find_parent_index(armature, to_index); + + if (from_parent_index != to_parent_index) { + /* Moving between parents. */ + int to_child_num = bonecoll_child_number(armature, to_parent_index, to_index); + if (before_after == MoveLocation::After) { + to_child_num++; + } + + return armature_bonecoll_move_to_parent( + armature, from_index, to_child_num, from_parent_index, to_parent_index); + } + + /* Moving between siblings. */ + switch (before_after) { + case MoveLocation::Before: + if (to_index > from_index) { + /* Moving to the right, but needs to go before that one, so needs a decrement. */ + to_index--; + } + break; + + case MoveLocation::After: + if (to_index < from_index) { + /* Moving to the left, but needs to go after that one, so needs a decrement. */ + to_index++; + } + break; + } + + if (!ANIM_armature_bonecoll_move_to_index(armature, from_index, to_index)) { + return -1; + } + return to_index; +} + bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, const int step) { if (bcoll == nullptr) { @@ -388,30 +591,61 @@ void ANIM_armature_bonecoll_name_set(bArmature *armature, BoneCollection *bcoll, BKE_animdata_fix_paths_rename_all(&armature->id, "collections", old_name, bcoll->name); } -void ANIM_armature_bonecoll_remove_from_index(bArmature *armature, const int index) +void ANIM_armature_bonecoll_remove_from_index(bArmature *armature, int index) { BLI_assert(0 <= index && index < armature->collection_array_num); BoneCollection *bcoll = armature->collection_array[index]; - /* Remove bone membership. */ - LISTBASE_FOREACH_MUTABLE (BoneCollectionMember *, member, &bcoll->bones) { - ANIM_armature_bonecoll_unassign(bcoll, member->bone); + /* The parent needs updating, so better to find it before this bone collection is removed. */ + int parent_bcoll_index = armature_bonecoll_find_parent_index(armature, index); + BoneCollection *parent_bcoll = parent_bcoll_index >= 0 ? + armature->collection_array[parent_bcoll_index] : + nullptr; + + /* Move all the children of the to-be-removed bone collection to their grandparent. */ + int move_to_child_num = bonecoll_child_number(armature, parent_bcoll_index, index); + while (bcoll->child_count > 0) { + /* Move the child to its grandparent, at the same spot as the to-be-removed + * bone collection. The latter thus (potentially) shifts by 1 in the array. + * After removal, this effectively makes it appear like the removed bone + * collection is replaced by all its children. */ + armature_bonecoll_move_to_parent(armature, + bcoll->child_index, /* Move from index... */ + move_to_child_num, /* to this child number. */ + index, /* From this parent... */ + parent_bcoll_index /* to that parent. */ + ); + + /* Both 'index' and 'parent_bcoll_index' can change each iteration. */ + index = internal::bonecolls_find_index_near(armature, bcoll, index); + BLI_assert_msg(index >= 0, "could not find bone collection after moving things around"); + + if (parent_bcoll_index >= 0) { /* If there is no parent, its index should stay -1. */ + parent_bcoll_index = internal::bonecolls_find_index_near( + armature, parent_bcoll, parent_bcoll_index); + BLI_assert_msg(parent_bcoll_index >= 0, + "could not find bone collection parent after moving things around"); + } + + move_to_child_num++; } - if (armature->edbo) { - LISTBASE_FOREACH (EditBone *, ebone, armature->edbo) { - ANIM_armature_bonecoll_unassign_editbone(bcoll, ebone); + + /* Adjust the parent for the removal of its child. */ + if (parent_bcoll_index < 0) { + /* Removing a root, so the armature itself needs to be updated. */ + armature->collection_root_count--; + BLI_assert_msg(armature->collection_root_count >= 0, "armature root count cannot be negative"); + } + else { + parent_bcoll->child_count--; + if (parent_bcoll->child_count == 0) { + parent_bcoll->child_index = 0; } } - ANIM_bonecoll_free(bcoll); - - /* Shift over the collections to fill the gap. */ - if (index < (armature->collection_array_num - 1)) { - BoneCollection **start = armature->collection_array + index; - const size_t count = armature->collection_array_num - index - 1; - memmove((void *)start, (void *)(start + 1), count * sizeof(BoneCollection *)); - } + /* Rotate the to-be-removed collection to the last array element. */ + internal::bonecolls_move_to_index(armature, index, armature->collection_array_num - 1); /* Note: we don't bother to shrink the allocation. It's okay if the * capacity has extra space, because the number of valid items is tracked. */ @@ -419,19 +653,31 @@ void ANIM_armature_bonecoll_remove_from_index(bArmature *armature, const int ind armature->collection_array[armature->collection_array_num] = nullptr; /* Update the active BoneCollection. */ - if (index <= armature->runtime.active_collection_index) { - int active_index = armature->runtime.active_collection_index; - - if (index == armature->collection_array_num) { - /* Removing the last element: activate the now-last element. */ - active_index--; + const int active_collection_index = armature->runtime.active_collection_index; + if (active_collection_index >= 0) { + /* Default: select the next sibling. + * If there is none: select the previous sibling. + * If there is none: select the parent. + */ + if (armature_bonecoll_is_child_of(armature, parent_bcoll_index, active_collection_index)) { + /* active_collection_index still points to a sibling of the removed collection. */ + ANIM_armature_bonecoll_active_index_set(armature, active_collection_index); } - else if (index < active_index) { - /* The active collection shifted, because a collection before it was removed. */ - active_index--; + else if (active_collection_index > 0 && + armature_bonecoll_is_child_of( + armature, parent_bcoll_index, active_collection_index - 1)) + { + /* The child preceeding active_collection_index is a sibling of the removed collection. */ + ANIM_armature_bonecoll_active_index_set(armature, active_collection_index - 1); + } + else { + /* Select the parent, or nothing if this was a root collection. In that case, if there are no + * siblings either, this just means all bone collections have been removed. */ + ANIM_armature_bonecoll_active_index_set(armature, parent_bcoll_index); } - ANIM_armature_bonecoll_active_index_set(armature, active_index); } + + internal::bonecoll_unassign_and_free(armature, bcoll); } void ANIM_armature_bonecoll_remove(bArmature *armature, BoneCollection *bcoll) @@ -450,6 +696,17 @@ BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature, const ch return nullptr; } +int ANIM_armature_bonecoll_get_index_by_name(bArmature *armature, const char *name) +{ + for (int index = 0; index < armature->collection_array_num; index++) { + const BoneCollection *bcoll = armature->collection_array[index]; + if (STREQ(bcoll->name, name)) { + return index; + } + } + return -1; +} + void ANIM_bonecoll_show(BoneCollection *bcoll) { bcoll->flags |= BONE_COLLECTION_VISIBLE; @@ -642,15 +899,39 @@ void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *e { if (armature->runtime.active_collection == nullptr) { /* No active collection, do not assign to any. */ - printf("ANIM_armature_bonecoll_assign_active(%s, %s): no active collection\n", - ebone->name, - armature->id.name); return; } ANIM_armature_bonecoll_assign_editbone(armature->runtime.active_collection, ebone); } +static bool bcoll_list_contains(const ListBase /*BoneCollectionRef*/ *collection_refs, + const BoneCollection *bcoll) +{ + LISTBASE_FOREACH (const BoneCollectionReference *, bcoll_ref, collection_refs) { + if (bcoll == bcoll_ref->bcoll) { + return true; + } + } + return false; +} + +bool ANIM_armature_bonecoll_contains_active_bone(const struct bArmature *armature, + const struct BoneCollection *bcoll) +{ + if (armature->edbo) { + if (!armature->act_edbone) { + return false; + } + return bcoll_list_contains(&armature->act_edbone->bone_collections, bcoll); + } + + if (!armature->act_bone) { + return false; + } + return bcoll_list_contains(&armature->act_bone->runtime.collections, bcoll); +} + void ANIM_armature_bonecoll_show_from_bone(bArmature *armature, const Bone *bone) { if (ANIM_bonecoll_is_visible(armature, bone)) { @@ -703,6 +984,170 @@ int armature_bonecoll_find_index(const bArmature *armature, const BoneCollection return -1; } +int armature_bonecoll_find_parent_index(const bArmature *armature, const int bcoll_index) +{ + if (bcoll_index < armature->collection_root_count) { + /* Don't bother iterating all collections when it's known to be a root. */ + return -1; + } + + int index = 0; + for (const BoneCollection *potential_parent : armature->collections_span()) { + if (potential_parent->child_index <= bcoll_index && + bcoll_index < potential_parent->child_index + potential_parent->child_count) + { + return index; + } + + index++; + } + + return -1; +} + +bool armature_bonecoll_is_root(const bArmature *armature, const int bcoll_index) +{ + BLI_assert(bcoll_index >= 0); + return bcoll_index < armature->collection_root_count; +} + +bool armature_bonecoll_is_child_of(const bArmature *armature, + const int potential_parent_index, + const int potential_child_index) +{ + /* Check for roots, before we try and access collection_array[-1]. */ + const bool is_root = armature_bonecoll_is_root(armature, potential_child_index); + if (is_root) { + return potential_parent_index == -1; + } + if (potential_parent_index < 0) { + return is_root; + } + + const BoneCollection *potential_parent = armature->collection_array[potential_parent_index]; + const int upper_bound = potential_parent->child_index + potential_parent->child_count; + + return potential_parent->child_index <= potential_child_index && + potential_child_index < upper_bound; +} + +bool armature_bonecoll_is_decendent_of(const bArmature *armature, + const int potential_parent_index, + const int potential_decendent_index) +{ + if (armature_bonecoll_is_child_of(armature, potential_parent_index, potential_decendent_index)) { + /* Found a direct child. */ + return true; + } + + const BoneCollection *potential_parent = armature->collection_array[potential_parent_index]; + const int upper_bound = potential_parent->child_index + potential_parent->child_count; + + for (int visit_index = potential_parent->child_index; visit_index < upper_bound; visit_index++) { + if (armature_bonecoll_is_decendent_of(armature, visit_index, potential_decendent_index)) { + return true; + } + } + + return false; +} + +bool bonecoll_has_children(const BoneCollection *bcoll) +{ + return bcoll->child_count > 0; +} + +int armature_bonecoll_move_to_parent(bArmature *armature, + const int from_bcoll_index, + int to_child_num, + const int from_parent_index, + const int to_parent_index) +{ + BLI_assert(0 <= from_bcoll_index && from_bcoll_index < armature->collection_array_num); + BLI_assert(-1 <= from_parent_index && from_parent_index < armature->collection_array_num); + BLI_assert(-1 <= to_parent_index && to_parent_index < armature->collection_array_num); + + if (from_parent_index == to_parent_index) { + /* TODO: use `to_child_num` to still move the child to the desired position. */ + return from_bcoll_index; + } + + /* The Armature itself acts like some sort of 'parent' for the root collections. By having this + * as a 'fake' BoneCollection, all the code below can just be blissfully unaware of the special + * 'all root collections should be at the start of the array' rule. */ + BoneCollection armature_root; + armature_root.child_count = armature->collection_root_count; + armature_root.child_index = 0; + + BoneCollection *from_parent = from_parent_index >= 0 ? + armature->collection_array[from_parent_index] : + &armature_root; + BoneCollection *to_parent = to_parent_index >= 0 ? armature->collection_array[to_parent_index] : + &armature_root; + + BLI_assert_msg(-1 <= to_child_num && to_child_num <= to_parent->child_count, + "to_child_num must point to an index of a child of the new parent, or the index " + "of the last child + 1, or be -1 to indicate 'after last child'"); + if (to_child_num < 0) { + to_child_num = to_parent->child_count; + } + + /* The new parent might not have children yet. */ + int to_bcoll_index; + if (to_parent->child_count == 0) { + /* New parents always get their children at the end of the array. */ + to_bcoll_index = armature->collection_array_num - 1; + } + else { + to_bcoll_index = to_parent->child_index + to_child_num; + + /* Check whether the new parent's children are to the left or right of bcoll_index. + * This determines which direction the collections have to shift, and thus which index to + * move the bcoll to. */ + if (to_bcoll_index > from_bcoll_index) { + to_bcoll_index--; + } + } + + /* In certain cases the 'from_parent' gets its first child removed, and needs to have its + * child_index incremented. This needs to be done by comparing these fields before the actual + * move happens (as that could also change the child_index). */ + const bool needs_post_move_child_index_bump = from_parent->child_index == from_bcoll_index && + to_bcoll_index <= from_bcoll_index; + /* bonecolls_move_to_index() will try and keep the hierarchy correct, and thus change + * to_parent->child_index to keep pointing to its current-first child. */ + const bool becomes_new_first_child = to_child_num == 0 || to_parent->child_count == 0; + internal::bonecolls_move_to_index(armature, from_bcoll_index, to_bcoll_index); + + /* Update child index & count of the old parent. */ + from_parent->child_count--; + if (from_parent->child_count == 0) { + /* Clean up the child index when the parent has no more children. */ + from_parent->child_index = 0; + } + else if (needs_post_move_child_index_bump) { + /* The start of the block of children of the old parent has moved, because + * we took out the first child. This only needs to be compensated for when + * moving it to the left (or staying put), as then its old siblings stay in + * place. + * + * This only needs to be done if there are any children left, though. */ + from_parent->child_index++; + } + + /* Update child index & count of the new parent. */ + if (becomes_new_first_child) { + to_parent->child_index = to_bcoll_index; + } + to_parent->child_count++; + + /* Copy the information from the 'fake' BoneCollection back to the armature. */ + armature->collection_root_count = armature_root.child_count; + BLI_assert(armature_root.child_index == 0); + + return to_bcoll_index; +} + /* Utility functions for Armature edit-mode undo. */ blender::Map ANIM_bonecoll_array_copy_no_membership( @@ -766,4 +1211,131 @@ void ANIM_bonecoll_array_free(BoneCollection ***bcoll_array, *bcoll_array_num = 0; } +/** Functions declared in bone_collections_internal.hh. */ +namespace internal { + +void bonecolls_rotate_block(bArmature *armature, + const int start_index, + const int count, + const int direction) +{ + BLI_assert_msg(direction == 1 || direction == -1, "`direction` must be either -1 or +1"); + + if (count == 0) { + return; + } + + /* When the block [start_index:start_index+count] is moved, it causes a duplication of one + * element and overwrites another element. For example: given an array [0, 1, 2, 3, 4], moving + * indices [1, 2] by +1 would result in one double element (1) and one missing element (3): [0, + * 1, 1, 2, 4]. + * + * This is resolved by moving that element to the other side of the block, so the result will be + * [0, 3, 1, 2, 4]. This breaks the hierarchical information, so it's up to the caller to update + * this one moved element. + */ + + const int move_from_index = (direction > 0 ? start_index + count : start_index - 1); + const int move_to_index = (direction > 0 ? start_index : start_index + count - 1); + BoneCollection *bcoll_to_move = armature->collection_array[move_from_index]; + + BoneCollection **start = armature->collection_array + start_index; + memmove((void *)(start + direction), (void *)start, count * sizeof(BoneCollection *)); + + armature->collection_array[move_to_index] = bcoll_to_move; + + /* Update all child indices that reference something in the moved block. */ + for (BoneCollection *bcoll : armature->collections_span()) { + /* Having both child_index and child_count zeroed out just means "no children"; these shouldn't + * be updated at all, as here child_index is not really referencing the element at index 0. */ + if (bcoll->child_index == 0 && bcoll->child_count == 0) { + continue; + } + + /* Compare to the original start & end of the block (i.e. pre-move). If a + * child_index is within this range, it'll need updating. */ + if (start_index <= bcoll->child_index && bcoll->child_index < start_index + count) { + bcoll->child_index += direction; + } + } +} + +void bonecolls_move_to_index(bArmature *armature, const int from_index, const int to_index) +{ + if (from_index == to_index) { + return; + } + + BLI_assert(0 <= from_index); + BLI_assert(from_index < armature->collection_array_num); + BLI_assert(0 <= to_index); + BLI_assert(to_index < armature->collection_array_num); + + if (from_index < to_index) { + const int block_start_index = from_index + 1; + const int block_count = to_index - from_index; + bonecolls_rotate_block(armature, block_start_index, block_count, -1); + } + else { + const int block_start_index = to_index; + const int block_count = from_index - to_index; + bonecolls_rotate_block(armature, block_start_index, block_count, +1); + } +} + +int bonecolls_find_index_near(bArmature *armature, BoneCollection *bcoll, const int index) +{ + BoneCollection **collections = armature->collection_array; + + if (collections[index] == bcoll) { + return index; + } + if (index > 0 && collections[index - 1] == bcoll) { + return index - 1; + } + if (index < armature->collection_array_num - 1 && collections[index + 1] == bcoll) { + return index + 1; + } + return -1; +} + +void bonecolls_debug_list(const bArmature *armature) +{ + printf("\033[38;5;214mBone collections of armature \"%s\":\033[0m\n", armature->id.name + 2); + constexpr int root_ansi_color = 95; + printf( + " - \033[%dmroot\033[0m count: %d\n", root_ansi_color, armature->collection_root_count); + for (int i = 0; i < armature->collection_array_num; ++i) { + const BoneCollection *bcoll = armature->collection_array[i]; + printf(" - \033[%dmcolls[%d] = %24s\033[0m ", + i < armature->collection_root_count ? root_ansi_color : 0, + i, + bcoll->name); + if (bcoll->child_index == 0 && bcoll->child_count == 0) { + printf("(leaf)"); + } + else { + printf("(child index: %d, count: %d)", bcoll->child_index, bcoll->child_count); + } + printf("\n"); + } +} + +void bonecoll_unassign_and_free(bArmature *armature, BoneCollection *bcoll) +{ + /* Remove bone membership. */ + LISTBASE_FOREACH_MUTABLE (BoneCollectionMember *, member, &bcoll->bones) { + ANIM_armature_bonecoll_unassign(bcoll, member->bone); + } + if (armature->edbo) { + LISTBASE_FOREACH (EditBone *, ebone, armature->edbo) { + ANIM_armature_bonecoll_unassign_editbone(bcoll, ebone); + } + } + + ANIM_bonecoll_free(bcoll); +} + +} // namespace internal + } // namespace blender::animrig diff --git a/source/blender/animrig/intern/bone_collections_internal.hh b/source/blender/animrig/intern/bone_collections_internal.hh new file mode 100644 index 00000000000..f722ff0fe28 --- /dev/null +++ b/source/blender/animrig/intern/bone_collections_internal.hh @@ -0,0 +1,70 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup animrig + * + * \brief Internal C++ functions to deal with bone collections. These are mostly here for internal + * use in `bone_collections.cc` and have them testable by unit tests. + */ + +#pragma once + +#include + +namespace blender::animrig::internal { + +/** + * Move a block of BoneCollections in the Armature's `collections_array`, from + * `start_index` to `start_index + direction`. + * + * The move operation is actually implemented as a rotation, so that no + * `BoneCollection*` is lost. In other words, one of these operations is + * performed, depending on `direction`. Here `B` indicates an element in the + * moved block, and `X` indicates the rotated element. + * + * direction = +1: [. . . X B B B B . . .] -> [. . . B B B B X . . .] + * direction = -1: [. . . B B B B X . . .] -> [. . . X B B B B . . .] + * + * This function does not alter the length of `collections_array`. + * It only performs the rotation, and updates any `child_index` when they + * reference elements of the moved block. + * + * It also does not touch any `child_count` properties of bone collections. + * Updating those, as well as any references to the rotated element, is the + * responsibility of the caller. + * + * \param direction must be either -1 or 1. + */ +void bonecolls_rotate_block(bArmature *armature, int start_index, int count, int direction); + +/** + * Move a bone collection to another index. + * + * This is implemented via a call to #bonecolls_rotate_block, so all the + * documentation of that function (including its invariants and caveats) applies + * here too. + */ +void bonecolls_move_to_index(bArmature *armature, int from_index, int to_index); + +/** + * Find the given bone collection in the armature's collections, and return its index. + * + * The bone collection is only searched for at the given index, index+1, and index-1. + * + * If the bone collection cannot be found, -1 is returned. + */ +int bonecolls_find_index_near(bArmature *armature, BoneCollection *bcoll, int index); + +void bonecolls_debug_list(const bArmature *armature); + +/** + * Unassign all (edit)bones from this bone collection, and free it. + * + * Note that this does NOT take care of updating the collection hierarchy information. See + * #ANIM_armature_bonecoll_remove_from_index and #ANIM_armature_bonecoll_remove for that. + */ +void bonecoll_unassign_and_free(bArmature *armature, BoneCollection *bcoll); + +} // namespace blender::animrig::internal diff --git a/source/blender/animrig/intern/bone_collections_test.cc b/source/blender/animrig/intern/bone_collections_test.cc index 34aac51d527..eb7ed7456d9 100644 --- a/source/blender/animrig/intern/bone_collections_test.cc +++ b/source/blender/animrig/intern/bone_collections_test.cc @@ -6,7 +6,11 @@ #include "BLT_translation.h" +#include "BKE_idtype.h" +#include "BKE_lib_id.h" + #include "ANIM_bone_collections.hh" +#include "intern/bone_collections_internal.hh" #include "testing/testing.h" @@ -49,6 +53,7 @@ class ANIM_armature_bone_collections : public testing::Test { memset(&bone2, 0, sizeof(Bone)); memset(&bone3, 0, sizeof(Bone)); + STRNCPY(arm.id.name, "ARArmature"); STRNCPY(bone1.name, "bone1"); STRNCPY(bone2.name, "bone2"); STRNCPY(bone3.name, "bone3"); @@ -62,13 +67,12 @@ class ANIM_armature_bone_collections : public testing::Test { void TearDown() override { - while (arm.collection_array_num > 0) { - ANIM_armature_bonecoll_remove_from_index(&arm, arm.collection_array_num - 1); - } - if (arm.collection_array) { - MEM_freeN(arm.collection_array); - } - BKE_armature_bone_hash_free(&arm); + /* Avoid freeing the bones, as they are part of this struct and not owned by + * the armature. */ + BLI_listbase_clear(&arm.bonebase); + + BKE_idtype_init(); + BKE_libblock_free_datablock(&arm.id, 0); } }; @@ -84,6 +88,371 @@ TEST_F(ANIM_armature_bone_collections, armature_owned_collections) ANIM_armature_bonecoll_remove(&arm, bcoll2); } +TEST_F(ANIM_armature_bone_collections, collection_hierarchy_creation) +{ + /* Implicit root: */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "wortel"); + /* Explicit root: */ + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "wortel", -1); + + ASSERT_EQ(0, armature_bonecoll_find_index(&arm, bcoll_root_0)); + ASSERT_EQ(1, armature_bonecoll_find_index(&arm, bcoll_root_1)); + + /* Child of bcoll at index 0: */ + BoneCollection *bcoll_child_of_0 = ANIM_armature_bonecoll_new(&arm, "koter", 0); + /* Child of bcoll at index 1: */ + BoneCollection *bcoll_child_of_1 = ANIM_armature_bonecoll_new(&arm, "koter", 1); + + ASSERT_EQ(4, arm.collection_array_num); + EXPECT_EQ(0, armature_bonecoll_find_index(&arm, bcoll_root_0)); + EXPECT_EQ(1, armature_bonecoll_find_index(&arm, bcoll_root_1)); + EXPECT_EQ(2, armature_bonecoll_find_index(&arm, bcoll_child_of_0)); + EXPECT_EQ(3, armature_bonecoll_find_index(&arm, bcoll_child_of_1)); + + /* Add another child of bcoll_root_0, which should push bcoll_child_of_1 further down the array. + */ + BoneCollection *bcoll_another_child_of_0 = ANIM_armature_bonecoll_new(&arm, "koter", 0); + ASSERT_EQ(5, arm.collection_array_num); + EXPECT_EQ(0, armature_bonecoll_find_index(&arm, bcoll_root_0)); + EXPECT_EQ(1, armature_bonecoll_find_index(&arm, bcoll_root_1)); + EXPECT_EQ(2, armature_bonecoll_find_index(&arm, bcoll_child_of_0)); + EXPECT_EQ(3, armature_bonecoll_find_index(&arm, bcoll_another_child_of_0)); + EXPECT_EQ(4, armature_bonecoll_find_index(&arm, bcoll_child_of_1)); + + /* Make sure the names remain unique within the entire Armature, and not just between siblings + * (i.e. a unique 'path' is not strong enough). */ + EXPECT_EQ(std::string("wortel"), std::string(bcoll_root_0->name)); + EXPECT_EQ(std::string("wortel.001"), std::string(bcoll_root_1->name)); + EXPECT_EQ(std::string("koter"), std::string(bcoll_child_of_0->name)); + EXPECT_EQ(std::string("koter.001"), std::string(bcoll_child_of_1->name)); + EXPECT_EQ(std::string("koter.002"), std::string(bcoll_another_child_of_0->name)); + + /* Test the internal hierarchy bookkeeping. */ + EXPECT_EQ(2, arm.collection_root_count); + EXPECT_EQ(2, bcoll_root_0->child_count); + EXPECT_EQ(1, bcoll_root_1->child_count); + EXPECT_EQ(0, bcoll_child_of_0->child_count); + EXPECT_EQ(0, bcoll_another_child_of_0->child_count); + EXPECT_EQ(0, bcoll_child_of_1->child_count); + + EXPECT_EQ(2, bcoll_root_0->child_index); + EXPECT_EQ(4, bcoll_root_1->child_index); + EXPECT_EQ(0, bcoll_child_of_0->child_index); + EXPECT_EQ(0, bcoll_another_child_of_0->child_index); + EXPECT_EQ(0, bcoll_child_of_1->child_index); + + /* TODO: test with deeper hierarchy. */ +} + +TEST_F(ANIM_armature_bone_collections, collection_hierarchy_removal) +{ + /* Set up a small hierarchy. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(5, arm.collection_array[1]->child_index); + ASSERT_EQ(0, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + + ASSERT_EQ(3, arm.collection_array[0]->child_count); + ASSERT_EQ(1, arm.collection_array[1]->child_count); + ASSERT_EQ(0, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + + /* Remove the middle child of root_0. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_r0_child1); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(5, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(4, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + + EXPECT_EQ(2, arm.collection_array[0]->child_count); + EXPECT_EQ(1, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + + /* Remove the first child of root_0. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_r0_child0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(4, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[3]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(3, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + + EXPECT_EQ(1, arm.collection_array[0]->child_count); + EXPECT_EQ(1, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + + /* Remove root_1 itself, which should make its only child a new root. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_root_1); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(3, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[2]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + + EXPECT_EQ(1, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); +} + +TEST_F(ANIM_armature_bone_collections, + collection_hierarchy_removal__more_complex_remove_inner_child) +{ + /* Set up a slightly bigger hierarchy. Contrary to the other tests these are + * actually declared in array order. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + BoneCollection *bcoll_r0c0_child0 = ANIM_armature_bonecoll_new(&arm, "r0c0_child0", 2); + BoneCollection *bcoll_r0c0_child1 = ANIM_armature_bonecoll_new(&arm, "r0c0_child1", 2); + BoneCollection *bcoll_r0c0_child2 = ANIM_armature_bonecoll_new(&arm, "r0c0_child2", 2); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(8, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children of root_0. */ + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[5]->name); /* Kids of r0_child0. */ + ASSERT_STREQ(bcoll_r0c0_child1->name, arm.collection_array[6]->name); + ASSERT_STREQ(bcoll_r0c0_child2->name, arm.collection_array[7]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(0, arm.collection_array[1]->child_index); + ASSERT_EQ(5, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + ASSERT_EQ(0, arm.collection_array[6]->child_index); + ASSERT_EQ(0, arm.collection_array[7]->child_index); + + ASSERT_EQ(3, arm.collection_array[0]->child_count); + ASSERT_EQ(0, arm.collection_array[1]->child_count); + ASSERT_EQ(3, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + ASSERT_EQ(0, arm.collection_array[6]->child_count); + ASSERT_EQ(0, arm.collection_array[7]->child_count); + + /* Remove bcoll_r0_child0, which should make all of its children a child of root_0. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_r0_child0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[2]->name); /* Children of root_0. */ + EXPECT_STREQ(bcoll_r0c0_child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0c0_child2->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[5]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[6]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + EXPECT_EQ(5, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* Remove root_0, which should make all of its children new roots. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_root_0); + + ASSERT_EQ(6, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_r0c0_child1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0c0_child2->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[5]->name); + + EXPECT_EQ(0, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(0, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); +} + +TEST_F(ANIM_armature_bone_collections, collection_hierarchy_removal__more_complex_remove_root) +{ + /* Set up a slightly bigger hierarchy. Contrary to the other tests these are + * actually declared in array order. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + BoneCollection *bcoll_r0c0_child0 = ANIM_armature_bonecoll_new(&arm, "r0c0_child0", 2); + BoneCollection *bcoll_r0c0_child1 = ANIM_armature_bonecoll_new(&arm, "r0c0_child1", 2); + BoneCollection *bcoll_r0c0_child2 = ANIM_armature_bonecoll_new(&arm, "r0c0_child2", 2); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(8, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children of root_0. */ + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[5]->name); /* Kids of r0_child0. */ + ASSERT_STREQ(bcoll_r0c0_child1->name, arm.collection_array[6]->name); + ASSERT_STREQ(bcoll_r0c0_child2->name, arm.collection_array[7]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(0, arm.collection_array[1]->child_index); + ASSERT_EQ(5, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + ASSERT_EQ(0, arm.collection_array[6]->child_index); + ASSERT_EQ(0, arm.collection_array[7]->child_index); + + ASSERT_EQ(3, arm.collection_array[0]->child_count); + ASSERT_EQ(0, arm.collection_array[1]->child_count); + ASSERT_EQ(3, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + ASSERT_EQ(0, arm.collection_array[6]->child_count); + ASSERT_EQ(0, arm.collection_array[7]->child_count); + + /* Remove root_0, which should make all of its children new roots. */ + ANIM_armature_bonecoll_remove(&arm, bcoll_root_0); + + ASSERT_EQ(4, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[4]->name); /* Kids of r0_child0. */ + ASSERT_STREQ(bcoll_r0c0_child1->name, arm.collection_array[5]->name); + ASSERT_STREQ(bcoll_r0c0_child2->name, arm.collection_array[6]->name); + + EXPECT_EQ(4, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); +} + +TEST_F(ANIM_armature_bone_collections, find_parent_index) +{ + /* Set up a small hierarchy. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0c0_child0 = ANIM_armature_bonecoll_new(&arm, "r0c0_child0", 2); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r0c0_child0->name, arm.collection_array[5]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(4, arm.collection_array[1]->child_index); + ASSERT_EQ(5, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + + ASSERT_EQ(2, arm.collection_array[0]->child_count); + ASSERT_EQ(1, arm.collection_array[1]->child_count); + ASSERT_EQ(1, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, -1)); + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, 500000)); + + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, 0)); + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, 1)); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 2)); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 3)); + EXPECT_EQ(1, armature_bonecoll_find_parent_index(&arm, 4)); + EXPECT_EQ(2, armature_bonecoll_find_parent_index(&arm, 5)); +} + TEST_F(ANIM_armature_bone_collections, bones_assign_unassign) { BoneCollection *bcoll = ANIM_armature_bonecoll_new(&arm, "collection"); @@ -216,25 +585,7 @@ TEST_F(ANIM_armature_bone_collections, bcoll_is_editable) << "Expecting local bone collection on local armature override to be editable"; } -TEST_F(ANIM_armature_bone_collections, bcoll_insert_copy_after) -{ - BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "collection"); - BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "collection"); - BoneCollection *bcoll3 = ANIM_armature_bonecoll_new(&arm, "collection"); - - EXPECT_EQ(arm.collection_array[0], bcoll1); - EXPECT_EQ(arm.collection_array[1], bcoll2); - EXPECT_EQ(arm.collection_array[2], bcoll3); - - BoneCollection *bcoll4 = ANIM_armature_bonecoll_insert_copy_after(&arm, bcoll2, bcoll2); - - EXPECT_EQ(arm.collection_array[0], bcoll1); - EXPECT_EQ(arm.collection_array[1], bcoll2); - EXPECT_EQ(arm.collection_array[2], bcoll4); - EXPECT_EQ(arm.collection_array[3], bcoll3); -} - -TEST_F(ANIM_armature_bone_collections, bcoll_move_to_index) +TEST_F(ANIM_armature_bone_collections, bcoll_move_to_index__roots) { BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "collection"); BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "collection"); @@ -246,14 +597,22 @@ TEST_F(ANIM_armature_bone_collections, bcoll_move_to_index) EXPECT_EQ(arm.collection_array[2], bcoll3); EXPECT_EQ(arm.collection_array[3], bcoll4); - ANIM_armature_bonecoll_move_to_index(&arm, 2, 1); + EXPECT_TRUE(ANIM_armature_bonecoll_move_to_index(&arm, 2, 1)); EXPECT_EQ(arm.collection_array[0], bcoll1); EXPECT_EQ(arm.collection_array[1], bcoll3); EXPECT_EQ(arm.collection_array[2], bcoll2); EXPECT_EQ(arm.collection_array[3], bcoll4); - ANIM_armature_bonecoll_move_to_index(&arm, 0, 3); + EXPECT_TRUE(ANIM_armature_bonecoll_move_to_index(&arm, 0, 3)); + + EXPECT_EQ(arm.collection_array[0], bcoll3); + EXPECT_EQ(arm.collection_array[1], bcoll2); + EXPECT_EQ(arm.collection_array[2], bcoll4); + EXPECT_EQ(arm.collection_array[3], bcoll1); + + /* Out of bounds should not be accepted. */ + EXPECT_FALSE(ANIM_armature_bonecoll_move_to_index(&arm, 0, 327)); EXPECT_EQ(arm.collection_array[0], bcoll3); EXPECT_EQ(arm.collection_array[1], bcoll2); @@ -261,4 +620,821 @@ TEST_F(ANIM_armature_bone_collections, bcoll_move_to_index) EXPECT_EQ(arm.collection_array[3], bcoll1); } +TEST_F(ANIM_armature_bone_collections, bcoll_move_to_index__siblings) +{ + BoneCollection *root = ANIM_armature_bonecoll_new(&arm, "root"); + BoneCollection *child0 = ANIM_armature_bonecoll_new(&arm, "child0", 0); + BoneCollection *child1 = ANIM_armature_bonecoll_new(&arm, "child1", 0); + BoneCollection *child2 = ANIM_armature_bonecoll_new(&arm, "child2", 0); + BoneCollection *child1_0 = ANIM_armature_bonecoll_new(&arm, "child1_0", 2); + + ASSERT_STREQ(root->name, arm.collection_array[0]->name); + ASSERT_STREQ(child0->name, arm.collection_array[1]->name); + ASSERT_STREQ(child1->name, arm.collection_array[2]->name); + ASSERT_STREQ(child2->name, arm.collection_array[3]->name); + ASSERT_STREQ(child1_0->name, arm.collection_array[4]->name); + + /* Move child2 to child0, i.e. a move 'to the left'. */ + EXPECT_TRUE(ANIM_armature_bonecoll_move_to_index(&arm, 3, 1)); + + EXPECT_STREQ(root->name, arm.collection_array[0]->name); + EXPECT_STREQ(child2->name, arm.collection_array[1]->name); + EXPECT_STREQ(child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(child1_0->name, arm.collection_array[4]->name); + + /* Move child2 to child1, i.e. a move 'to the right'. */ + EXPECT_TRUE(ANIM_armature_bonecoll_move_to_index(&arm, 1, 3)); + + EXPECT_STREQ(root->name, arm.collection_array[0]->name); + EXPECT_STREQ(child0->name, arm.collection_array[1]->name); + EXPECT_STREQ(child1->name, arm.collection_array[2]->name); + EXPECT_STREQ(child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(child1_0->name, arm.collection_array[4]->name); + + /* Move child2 to root, should not be allowed. */ + EXPECT_FALSE(ANIM_armature_bonecoll_move_to_index(&arm, 3, 0)); + + EXPECT_STREQ(root->name, arm.collection_array[0]->name); + EXPECT_STREQ(child0->name, arm.collection_array[1]->name); + EXPECT_STREQ(child1->name, arm.collection_array[2]->name); + EXPECT_STREQ(child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(child1_0->name, arm.collection_array[4]->name); + + /* Move child1_0 to child_2, should not be allowed. */ + EXPECT_FALSE(ANIM_armature_bonecoll_move_to_index(&arm, 4, 3)); + + EXPECT_STREQ(root->name, arm.collection_array[0]->name); + EXPECT_STREQ(child0->name, arm.collection_array[1]->name); + EXPECT_STREQ(child1->name, arm.collection_array[2]->name); + EXPECT_STREQ(child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(child1_0->name, arm.collection_array[4]->name); +} + +TEST_F(ANIM_armature_bone_collections, bcoll_move_to_parent) +{ + /* Set up a small hierarchy. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(5, arm.collection_array[1]->child_index); + ASSERT_EQ(0, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + + ASSERT_EQ(3, arm.collection_array[0]->child_count); + ASSERT_EQ(1, arm.collection_array[1]->child_count); + ASSERT_EQ(0, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + + /* Move the middle child of root_0 to root_1. */ + EXPECT_EQ(5, armature_bonecoll_move_to_parent(&arm, 3, bcoll_root_1->child_count, 0, 1)); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[5]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(4, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(2, arm.collection_array[0]->child_count); + EXPECT_EQ(2, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + + /* Move the first child of root_1 to root_0. This shouldn't change its index. */ + EXPECT_EQ(4, armature_bonecoll_move_to_parent(&arm, 4, bcoll_root_0->child_count, 1, 0)); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[5]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(5, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(1, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + + /* Move the final child of root_1 to root_0. This shouldn't change its index + * again, but leave root_1 without children. */ + EXPECT_EQ(5, armature_bonecoll_move_to_parent(&arm, 5, bcoll_root_0->child_count, 1, 0)); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[5]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(4, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + + /* Move the first child of root_0 (bcoll_r0_child0) to bcoll_r0_child2. */ + EXPECT_EQ(5, armature_bonecoll_move_to_parent(&arm, 2, bcoll_r0_child2->child_count, 0, 3)); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[5]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(5, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(1, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); +} + +TEST_F(ANIM_armature_bone_collections, bcoll_move_to_parent__root_unroot) +{ + /* Set up a small hierarchy. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(5, arm.collection_array[1]->child_index); + ASSERT_EQ(0, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + + ASSERT_EQ(3, arm.collection_array[0]->child_count); + ASSERT_EQ(1, arm.collection_array[1]->child_count); + ASSERT_EQ(0, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + + /* Make a leaf node (bcoll_r0_child1) a root. */ + EXPECT_EQ(2, armature_bonecoll_move_to_parent(&arm, 3, arm.collection_root_count, 0, -1)); + + ASSERT_EQ(3, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[2]->name); // Became a root. + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + EXPECT_EQ(3, arm.collection_array[0]->child_index); + EXPECT_EQ(5, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(2, arm.collection_array[0]->child_count); + EXPECT_EQ(1, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + + /* Make a root node (root_1) a child of root_0. */ + EXPECT_EQ(4, armature_bonecoll_move_to_parent(&arm, 1, bcoll_root_0->child_count, -1, 0)); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(6, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[1]->name); // Actually a root. + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[4]->name); // Became a child. + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + EXPECT_EQ(2, arm.collection_array[0]->child_index); + EXPECT_EQ(0, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(5, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(0, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(1, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + + // TODO: test with circular parenthood. +} + +TEST_F(ANIM_armature_bone_collections, bcoll_move_to_parent__within_siblings) +{ + /* Set up a small hierarchy. */ + auto bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + auto bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + auto bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + auto bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + auto bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + auto bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + auto bcoll_r0_child3 = ANIM_armature_bonecoll_new(&arm, "r0_child3", 0); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[3]->name); /* Children root_0. */ + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[5]->name); + ASSERT_STREQ(bcoll_r0_child3->name, arm.collection_array[6]->name); + + ASSERT_EQ(3, arm.collection_array[0]->child_index); + ASSERT_EQ(2, arm.collection_array[1]->child_index); + ASSERT_EQ(0, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + ASSERT_EQ(0, arm.collection_array[6]->child_index); + + ASSERT_EQ(4, arm.collection_array[0]->child_count); + ASSERT_EQ(1, arm.collection_array[1]->child_count); + ASSERT_EQ(0, arm.collection_array[2]->child_count); + ASSERT_EQ(0, arm.collection_array[3]->child_count); + ASSERT_EQ(0, arm.collection_array[4]->child_count); + ASSERT_EQ(0, arm.collection_array[5]->child_count); + ASSERT_EQ(0, arm.collection_array[6]->child_count); + + /* First half of the test, move 3 children from root_1 to root_0. */ + + /* Move r0_child0 to become 1st child of root_1, before r1_child0. */ + EXPECT_EQ(2, + armature_bonecoll_move_to_parent(&arm, + 3, /* From index.*/ + 0, /* To child number.*/ + 0, /* From parent. */ + 1 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[4]->name); /* Children root_0. */ + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[5]->name); + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[6]->name); + + EXPECT_EQ(4, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(2, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* Move r0_child1 to become the 2nd child of root_1. */ + EXPECT_EQ(3, + armature_bonecoll_move_to_parent(&arm, + 4, /* From index.*/ + 1, /* To child number.*/ + 0, /* From parent. */ + 1 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[5]->name); /* Children root_0. */ + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[6]->name); + + EXPECT_EQ(5, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(2, arm.collection_array[0]->child_count); + EXPECT_EQ(3, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* Move r0_child3 to become the last child of root_1. */ + EXPECT_EQ(5, + armature_bonecoll_move_to_parent(&arm, + 6, /* From index.*/ + 3, /* To child number.*/ + 0, /* From parent. */ + 1 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[5]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[6]->name); /* Children root_0. */ + + EXPECT_EQ(6, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(1, arm.collection_array[0]->child_count); + EXPECT_EQ(4, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* 2nd half of the test: move the children back to root_0 to test moving in + * the other direction. */ + + /* Move r0_child3 to become the first child of root_0. */ + EXPECT_EQ(5, + armature_bonecoll_move_to_parent(&arm, + 5, /* From index.*/ + 0, /* To child number.*/ + 1, /* From parent. */ + 0 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[5]->name); /* Children root_0. */ + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[6]->name); + + EXPECT_EQ(5, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(2, arm.collection_array[0]->child_count); + EXPECT_EQ(3, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* Move r0_child0 to become the last child of root_0. */ + EXPECT_EQ(6, + armature_bonecoll_move_to_parent(&arm, + 2, /* From index.*/ + 2, /* To child number.*/ + 1, /* From parent. */ + 0 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[4]->name); /* Children root_0. */ + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[5]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[6]->name); + + EXPECT_EQ(4, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(3, arm.collection_array[0]->child_count); + EXPECT_EQ(2, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); + + /* Move r0_child1 to become the 3nd child of root_0. */ + EXPECT_EQ(5, + armature_bonecoll_move_to_parent(&arm, + 2, /* From index.*/ + 2, /* To child number.*/ + 1, /* From parent. */ + 0 /* To parent.*/ + )); + + ASSERT_EQ(2, arm.collection_root_count); + ASSERT_EQ(7, arm.collection_array_num); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[2]->name); /* Children root_1. */ + EXPECT_STREQ(bcoll_r0_child3->name, arm.collection_array[3]->name); /* Children root_0. */ + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[5]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[6]->name); + + EXPECT_EQ(3, arm.collection_array[0]->child_index); + EXPECT_EQ(2, arm.collection_array[1]->child_index); + EXPECT_EQ(0, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + EXPECT_EQ(0, arm.collection_array[6]->child_index); + + EXPECT_EQ(4, arm.collection_array[0]->child_count); + EXPECT_EQ(1, arm.collection_array[1]->child_count); + EXPECT_EQ(0, arm.collection_array[2]->child_count); + EXPECT_EQ(0, arm.collection_array[3]->child_count); + EXPECT_EQ(0, arm.collection_array[4]->child_count); + EXPECT_EQ(0, arm.collection_array[5]->child_count); + EXPECT_EQ(0, arm.collection_array[6]->child_count); +} + +TEST_F(ANIM_armature_bone_collections, internal__bonecolls_rotate_block) +{ + /* Set up a small hierarchy. */ + BoneCollection *bcoll_root_0 = ANIM_armature_bonecoll_new(&arm, "root_0"); + BoneCollection *bcoll_root_1 = ANIM_armature_bonecoll_new(&arm, "root_1"); + BoneCollection *bcoll_r0_child0 = ANIM_armature_bonecoll_new(&arm, "r0_child0", 0); + BoneCollection *bcoll_r1_child0 = ANIM_armature_bonecoll_new(&arm, "r1_child0", 1); + BoneCollection *bcoll_r0_child1 = ANIM_armature_bonecoll_new(&arm, "r0_child1", 0); + BoneCollection *bcoll_r0_child2 = ANIM_armature_bonecoll_new(&arm, "r0_child2", 0); + + /* The tests below compare the collection names, instead of their pointers, so + * that we get human-readable messages on failure. */ + + /* Unnecessary assertions, just to make it easier to understand in which order + * the array starts out. */ + ASSERT_EQ(6, arm.collection_array_num); + ASSERT_STREQ(bcoll_root_0->name, arm.collection_array[0]->name); + ASSERT_STREQ(bcoll_root_1->name, arm.collection_array[1]->name); + ASSERT_STREQ(bcoll_r0_child0->name, arm.collection_array[2]->name); + ASSERT_STREQ(bcoll_r0_child1->name, arm.collection_array[3]->name); + ASSERT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + ASSERT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + ASSERT_EQ(2, arm.collection_array[0]->child_index); + ASSERT_EQ(5, arm.collection_array[1]->child_index); + ASSERT_EQ(0, arm.collection_array[2]->child_index); + ASSERT_EQ(0, arm.collection_array[3]->child_index); + ASSERT_EQ(0, arm.collection_array[4]->child_index); + ASSERT_EQ(0, arm.collection_array[5]->child_index); + + /* Move [0,1,2] to [1,2,3]. */ + internal::bonecolls_rotate_block(&arm, 0, 3, 1); + ASSERT_EQ(6, arm.collection_array_num) << "array size should not change"; + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[5]->name); + + EXPECT_EQ(0, arm.collection_array[0]->child_index); + EXPECT_EQ(3, arm.collection_array[1]->child_index); + EXPECT_EQ(5, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); + + /* Move [4,5] to [3,4]. */ + internal::bonecolls_rotate_block(&arm, 4, 2, -1); + ASSERT_EQ(6, arm.collection_array_num) << "array size should not change"; + EXPECT_STREQ(bcoll_r0_child1->name, arm.collection_array[0]->name); + EXPECT_STREQ(bcoll_root_0->name, arm.collection_array[1]->name); + EXPECT_STREQ(bcoll_root_1->name, arm.collection_array[2]->name); + EXPECT_STREQ(bcoll_r0_child2->name, arm.collection_array[3]->name); + EXPECT_STREQ(bcoll_r1_child0->name, arm.collection_array[4]->name); + EXPECT_STREQ(bcoll_r0_child0->name, arm.collection_array[5]->name); + + EXPECT_EQ(0, arm.collection_array[0]->child_index); + EXPECT_EQ(3, arm.collection_array[1]->child_index); + EXPECT_EQ(4, arm.collection_array[2]->child_index); + EXPECT_EQ(0, arm.collection_array[3]->child_index); + EXPECT_EQ(0, arm.collection_array[4]->child_index); + EXPECT_EQ(0, arm.collection_array[5]->child_index); +} + +class ANIM_armature_bone_collections_testlist : public testing::Test { + protected: + bArmature arm; + + BoneCollection *root; + BoneCollection *child0; + BoneCollection *child1; + BoneCollection *child2; + BoneCollection *child1_0; + + void SetUp() override + { + memset(&arm, 0, sizeof(arm)); + STRNCPY(arm.id.name, "ARArmature"); + + root = ANIM_armature_bonecoll_new(&arm, "root"); + child0 = ANIM_armature_bonecoll_new(&arm, "child0", 0); + child1 = ANIM_armature_bonecoll_new(&arm, "child1", 0); + child2 = ANIM_armature_bonecoll_new(&arm, "child2", 0); + child1_0 = ANIM_armature_bonecoll_new(&arm, "child1_0", 2); + + ASSERT_STREQ(root->name, arm.collection_array[0]->name); + ASSERT_STREQ(child0->name, arm.collection_array[1]->name); + ASSERT_STREQ(child1->name, arm.collection_array[2]->name); + ASSERT_STREQ(child2->name, arm.collection_array[3]->name); + ASSERT_STREQ(child1_0->name, arm.collection_array[4]->name); + } + + void TearDown() override + { + BKE_idtype_init(); + BKE_libblock_free_datablock(&arm.id, 0); + } + + void expect_bcolls(std::initializer_list expect_names) + { + EXPECT_EQ(expect_names.size(), arm.collection_array_num); + + int index = 0; + for (const std::string &expect_bcoll : expect_names) { + BoneCollection *actual_bcoll = arm.collection_array[index]; + EXPECT_EQ(expect_bcoll, std::string(actual_bcoll->name)) + << "Expected collection_array[" << index << "] to be " << expect_bcoll << ", but it is " + << actual_bcoll->name; + index++; + } + } +}; + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__before_first_sibling) +{ + EXPECT_EQ(1, ANIM_armature_bonecoll_move_before_after_index(&arm, 3, 1, MoveLocation::Before)); + expect_bcolls({"root", "child2", "child0", "child1", "child1_0"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 1)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__after_first_sibling) +{ + EXPECT_EQ(2, ANIM_armature_bonecoll_move_before_after_index(&arm, 3, 1, MoveLocation::After)); + expect_bcolls({"root", "child0", "child2", "child1", "child1_0"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 2)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__before_last_sibling) +{ + EXPECT_EQ(2, ANIM_armature_bonecoll_move_before_after_index(&arm, 1, 3, MoveLocation::Before)); + expect_bcolls({"root", "child1", "child0", "child2", "child1_0"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 2)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__after_last_sibling) +{ + EXPECT_EQ(3, ANIM_armature_bonecoll_move_before_after_index(&arm, 1, 3, MoveLocation::After)); + expect_bcolls({"root", "child1", "child2", "child0", "child1_0"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 3)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, + move_before_after_index__other_parent_before__move_left) +{ + EXPECT_EQ(1, ANIM_armature_bonecoll_move_before_after_index(&arm, 4, 1, MoveLocation::Before)); + expect_bcolls({"root", "child1_0", "child0", "child1", "child2"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 1)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, + move_before_after_index__other_parent_after__move_left) +{ + EXPECT_EQ(2, ANIM_armature_bonecoll_move_before_after_index(&arm, 4, 1, MoveLocation::After)); + expect_bcolls({"root", "child0", "child1_0", "child1", "child2"}); + EXPECT_EQ(0, armature_bonecoll_find_parent_index(&arm, 2)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, + move_before_after_index__other_parent_before__move_right) +{ + EXPECT_EQ(3, ANIM_armature_bonecoll_move_before_after_index(&arm, 1, 4, MoveLocation::Before)); + expect_bcolls({"root", "child1", "child2", "child0", "child1_0"}); + EXPECT_EQ(1, armature_bonecoll_find_parent_index(&arm, 3)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, + move_before_after_index__other_parent_after__move_right) +{ + EXPECT_EQ(4, ANIM_armature_bonecoll_move_before_after_index(&arm, 1, 4, MoveLocation::After)); + expect_bcolls({"root", "child1", "child2", "child1_0", "child0"}); + EXPECT_EQ(1, armature_bonecoll_find_parent_index(&arm, 4)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__to_root__before) +{ + EXPECT_EQ(0, ANIM_armature_bonecoll_move_before_after_index(&arm, 4, 0, MoveLocation::Before)); + expect_bcolls({"child1_0", "root", "child0", "child1", "child2"}); + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, 0)); +} + +TEST_F(ANIM_armature_bone_collections_testlist, move_before_after_index__to_root__after) +{ + EXPECT_EQ(1, ANIM_armature_bonecoll_move_before_after_index(&arm, 4, 0, MoveLocation::After)); + expect_bcolls({"root", "child1_0", "child0", "child1", "child2"}); + EXPECT_EQ(-1, armature_bonecoll_find_parent_index(&arm, 1)); +} + +class ANIM_armature_bone_collections_liboverrides + : public ANIM_armature_bone_collections_testlist { + protected: + bArmature dst_arm; + + BoneCollection *dst_root; + BoneCollection *dst_child0; + BoneCollection *dst_child1; + BoneCollection *dst_child2; + BoneCollection *dst_child1_0; + + void SetUp() override + { + ANIM_armature_bone_collections_testlist::SetUp(); + + /* TODO: make this clone `arm` into `dst_arm`, instead of assuming the below + * code is still in sync with the superclass. */ + memset(&dst_arm, 0, sizeof(dst_arm)); + STRNCPY(dst_arm.id.name, "ARArmatureDST"); + + dst_root = ANIM_armature_bonecoll_new(&dst_arm, "root"); + dst_child0 = ANIM_armature_bonecoll_new(&dst_arm, "child0", 0); + dst_child1 = ANIM_armature_bonecoll_new(&dst_arm, "child1", 0); + dst_child2 = ANIM_armature_bonecoll_new(&dst_arm, "child2", 0); + dst_child1_0 = ANIM_armature_bonecoll_new(&dst_arm, "child1_0", 2); + + ASSERT_STREQ(dst_root->name, dst_arm.collection_array[0]->name); + ASSERT_STREQ(dst_child0->name, dst_arm.collection_array[1]->name); + ASSERT_STREQ(dst_child1->name, dst_arm.collection_array[2]->name); + ASSERT_STREQ(dst_child2->name, dst_arm.collection_array[3]->name); + ASSERT_STREQ(dst_child1_0->name, dst_arm.collection_array[4]->name); + + BKE_armature_bone_hash_make(&arm); + BKE_armature_bone_hash_make(&dst_arm); + } + + void TearDown() override + { + ANIM_armature_bone_collections_testlist::TearDown(); + BKE_libblock_free_datablock(&dst_arm.id, 0); + } +}; + +TEST_F(ANIM_armature_bone_collections_liboverrides, bcoll_insert_copy_after) +{ + /* Mimick that a new root, two children, and two grandchildren were added via library overrides. + * These were saved in `arm`, and now need to be copied into `dst_arm`. */ + BoneCollection *src_root = ANIM_armature_bonecoll_new(&arm, "new_root"); + const int root_index = armature_bonecoll_find_index(&arm, src_root); + BoneCollection *src_child1 = ANIM_armature_bonecoll_new(&arm, "new_child1", root_index); + ANIM_armature_bonecoll_new(&arm, "new_child2", root_index); + const int child1_index = armature_bonecoll_find_index(&arm, src_child1); + ANIM_armature_bonecoll_new(&arm, "new_gchild1", child1_index); + ANIM_armature_bonecoll_new(&arm, "new_gchild2", child1_index); + + /* Copy the root. This should be the only change that's recorded by a library override operation. + * It should also copy the entire subtree of that root. */ + const BoneCollection *anchor = dst_arm.collection_array[0]; + ASSERT_STREQ("root", anchor->name); + BoneCollection *copy_root = ANIM_armature_bonecoll_insert_copy_after( + &dst_arm, &arm, anchor, src_root); + + /* Check the array order. */ + expect_bcolls({"root", + "new_root", + "child0", + "child1", + "child2", + "child1_0", + "new_child1", + "new_child2", + "new_gchild1", + "new_gchild2"}); + + /* Check that the copied root is actually stored in the destination armature array. */ + const int new_root_index = armature_bonecoll_find_index(&dst_arm, copy_root); + EXPECT_EQ(1, new_root_index); + + /* Check the hierarchy. */ + const int new_child1_index = ANIM_armature_bonecoll_get_index_by_name(&dst_arm, "new_child1"); + EXPECT_TRUE(armature_bonecoll_is_root(&dst_arm, new_root_index)); + EXPECT_TRUE(armature_bonecoll_is_child_of(&dst_arm, new_root_index, new_child1_index)); + EXPECT_TRUE(armature_bonecoll_is_child_of( + &dst_arm, new_root_index, ANIM_armature_bonecoll_get_index_by_name(&dst_arm, "new_child2"))); + EXPECT_TRUE(armature_bonecoll_is_child_of( + &dst_arm, + new_child1_index, + ANIM_armature_bonecoll_get_index_by_name(&dst_arm, "new_gchild1"))); + EXPECT_TRUE(armature_bonecoll_is_child_of( + &dst_arm, + new_child1_index, + ANIM_armature_bonecoll_get_index_by_name(&dst_arm, "new_gchild2"))); + + if (HasFailure()) { + internal::bonecolls_debug_list(&dst_arm); + } +} + } // namespace blender::animrig::tests diff --git a/source/blender/blenkernel/intern/armature.cc b/source/blender/blenkernel/intern/armature.cc index 0376c5d4efd..8f83d07c4d4 100644 --- a/source/blender/blenkernel/intern/armature.cc +++ b/source/blender/blenkernel/intern/armature.cc @@ -388,8 +388,26 @@ static void read_bone_collections(BlendDataReader *reader, bArmature *arm) arm->collection_array_num, sizeof(BoneCollection *), __func__); { int i; + int min_child_index = 0; LISTBASE_FOREACH_INDEX (BoneCollection *, bcoll, &arm->collections_legacy, i) { arm->collection_array[i] = bcoll; + + if (bcoll->child_index > 0) { + min_child_index = min_ii(min_child_index, bcoll->child_index); + } + } + + if (arm->collection_root_count == 0 && arm->collection_array_num > 0) { + /* There cannot be zero roots when there are any bone collections. This means the root count + * likely got lost for some reason, and should be reconstructed to avoid data corruption when + * modifying the array. */ + if (min_child_index == 0) { + /* None of the bone collections had any children, so all are roots. */ + arm->collection_root_count = arm->collection_array_num; + } + else { + arm->collection_root_count = min_child_index; + } } } diff --git a/source/blender/editors/armature/bone_collections.cc b/source/blender/editors/armature/bone_collections.cc index 8b4d073302e..437afe13ba4 100644 --- a/source/blender/editors/armature/bone_collections.cc +++ b/source/blender/editors/armature/bone_collections.cc @@ -9,6 +9,8 @@ #include +#include "BLI_string.h" + #include "ANIM_bone_collections.hh" #include "DNA_ID.h" @@ -109,7 +111,7 @@ static bool active_bone_collection_poll(bContext *C) return true; } -static int bone_collection_add_exec(bContext *C, wmOperator * /*op*/) +static int bone_collection_add_exec(bContext *C, wmOperator *op) { Object *ob = ED_object_context(C); if (ob == nullptr) { @@ -117,7 +119,24 @@ static int bone_collection_add_exec(bContext *C, wmOperator * /*op*/) } bArmature *armature = static_cast(ob->data); - BoneCollection *bcoll = ANIM_armature_bonecoll_new(armature, nullptr); + + const int parent_index = RNA_int_get(op->ptr, "parent_index"); + printf("Adding bone collection to parent index %d\n", parent_index); + if (parent_index < -1) { + BKE_reportf( + op->reports, RPT_ERROR, "parent_index should not be less than -1: %d", parent_index); + return OPERATOR_CANCELLED; + } + if (parent_index >= armature->collection_array_num) { + BKE_reportf(op->reports, + RPT_ERROR, + "parent_index (%d) should be less than the number of bone collections (%d)", + parent_index, + armature->collection_array_num); + return OPERATOR_CANCELLED; + } + + BoneCollection *bcoll = ANIM_armature_bonecoll_new(armature, nullptr, parent_index); ANIM_armature_bonecoll_active_set(armature, bcoll); WM_event_add_notifier(C, NC_OBJECT | ND_POSE, ob); @@ -137,6 +156,19 @@ void ARMATURE_OT_collection_add(wmOperatorType *ot) /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + PropertyRNA *prop; + prop = RNA_def_int( + ot->srna, + "parent_index", + -1, + -1, + INT_MAX, + "Parent Index", + "Index of the parent bone collection, or -1 if the new bone collection should be a root", + -1, + INT_MAX); + RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); } static int bone_collection_remove_exec(bContext *C, wmOperator * /*op*/) @@ -189,7 +221,9 @@ static int bone_collection_move_exec(bContext *C, wmOperator *op) return OPERATOR_CANCELLED; } - WM_event_add_notifier(C, NC_OBJECT | ND_POSE, ob); + ANIM_armature_bonecoll_active_runtime_refresh(armature); + + WM_event_add_notifier(C, NC_OBJECT | ND_BONE_COLLECTION, ob); return OPERATOR_FINISHED; } @@ -460,8 +494,6 @@ void ARMATURE_OT_collection_assign(wmOperatorType *ot) ot->description = "Add selected bones to the chosen bone collection"; /* api callbacks */ - // TODO: reinstate the menu? - // ot->invoke = bone_collections_menu_invoke; ot->exec = bone_collection_assign_exec; ot->poll = bone_collection_assign_poll; @@ -861,14 +893,15 @@ void ARMATURE_OT_collection_deselect(wmOperatorType *ot) static BoneCollection *add_or_move_to_collection_bcoll(wmOperator *op, bArmature *arm) { - const int collection_index = RNA_enum_get(op->ptr, "collection"); + const int collection_index = RNA_int_get(op->ptr, "collection_index"); BoneCollection *target_bcoll; - if (collection_index < 0) { + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "new_collection_name"); + if (RNA_property_is_set(op->ptr, prop)) { /* TODO: check this with linked, non-overridden armatures. */ char new_collection_name[MAX_NAME]; RNA_string_get(op->ptr, "new_collection_name", new_collection_name); - target_bcoll = ANIM_armature_bonecoll_new(arm, new_collection_name); + target_bcoll = ANIM_armature_bonecoll_new(arm, new_collection_name, collection_index); BLI_assert_msg(target_bcoll, "It should always be possible to create a new bone collection on an armature"); ANIM_armature_bonecoll_active_set(arm, target_bcoll); @@ -988,98 +1021,164 @@ static bool move_to_collection_poll(bContext *C) return true; } -static bool bone_collection_enum_itemf_for_object(Object *ob, - EnumPropertyItem **item, - int *totitem) +/** + * Encode the parameters into an integer, and return as void*. + * + * This makes it possible to use these values and pass them directly as 'custom data' pointer to + * `uiItemMenuF()`. This makes it possible to give every menu a unique bone collection index for + * which it should show the child collections, without having to allocate memory or use static + * variables. See `move_to_collection_invoke()` in `object_edit.cc` for the alternative that I + * (Sybren) wanted to avoid. */ +static void *menu_custom_data_encode(const int bcoll_index, const bool is_move_operation) { - EnumPropertyItem item_tmp = {0}; - bArmature *arm = static_cast(ob->data); - - for (int bcoll_index = 0; bcoll_index < arm->collection_array_num; bcoll_index++) { - BoneCollection *bcoll = arm->collection_array[bcoll_index]; - if (!ANIM_armature_bonecoll_is_editable(arm, bcoll)) { - /* Skip bone collections that cannot be assigned to because they're - * linked and thus uneditable. If there is a way to still show these, but in a disabled - * state, that would be preferred. */ - continue; - } - item_tmp.identifier = bcoll->name; - item_tmp.name = bcoll->name; - item_tmp.value = bcoll_index; - RNA_enum_item_add(item, totitem, &item_tmp); - } - - return true; + /* Add 1 to the index, so that it's never negative (it can be -1 to indicate 'all roots'). */ + const uintptr_t index_and_move_bit = ((bcoll_index + 1) << 1) | (is_move_operation << 0); + return reinterpret_cast(index_and_move_bit); } -static const EnumPropertyItem *bone_collection_enum_itemf(bContext *C, - PointerRNA * /*ptr*/, - PropertyRNA * /*prop*/, - bool *r_free) +/** + * Decode the `void*` back into a bone collection index and a boolean `is_move_operation`. + * + * \see menu_custom_data_encode for rationale. + */ +static std::pair menu_custom_data_decode(void *menu_custom_data) { - *r_free = false; + const uintptr_t index_and_move_bit = reinterpret_cast(menu_custom_data); + const bool is_move_operation = (index_and_move_bit & 1) == 1; + const int bcoll_index = int(index_and_move_bit >> 1) - 1; + return std::make_pair(bcoll_index, is_move_operation); +} - if (!C) { - /* This happens when operators are being tested, and not during normal invocation. */ - return rna_enum_dummy_NULL_items; +static int icon_for_bone_collection(const bool collection_contains_active_bone) +{ + return collection_contains_active_bone ? ICON_REMOVE : ICON_ADD; +} + +static void menu_add_item_for_move_assign_unassign(uiLayout *layout, + const bArmature *arm, + const BoneCollection *bcoll, + const int bcoll_index, + const bool is_move_operation) +{ + if (is_move_operation) { + uiItemIntO(layout, + bcoll->name, + ICON_NONE, + "ARMATURE_OT_move_to_collection", + "collection_index", + bcoll_index); + return; } - Object *ob = ED_object_context(C); - if (!ob || ob->type != OB_ARMATURE) { - return rna_enum_dummy_NULL_items; + const bool contains_active_bone = ANIM_armature_bonecoll_contains_active_bone(arm, bcoll); + const int icon = icon_for_bone_collection(contains_active_bone); + + if (contains_active_bone) { + uiItemStringO( + layout, bcoll->name, icon, "ARMATURE_OT_collection_unassign", "name", bcoll->name); + } + else { + uiItemStringO(layout, bcoll->name, icon, "ARMATURE_OT_collection_assign", "name", bcoll->name); + } +} + +/** + * Add menu items to the layout, for a set of bone collections. + * + * \param menu_custom_data contains two values, encoded as void* to match the signature required by + * `uiItemMenuF`. It contains the parent bone collection index (either -1 to show all roots, or + * another value to show the children of that collection), as well as a boolean that indicates + * whether the menu is created for the "move to collection" or "assign to collection" operator. + * + * \see menu_custom_data_encode + */ +static void move_to_collection_menu_create(bContext *C, uiLayout *layout, void *menu_custom_data) +{ + int parent_bcoll_index; + bool is_move_operation; + std::tie(parent_bcoll_index, is_move_operation) = menu_custom_data_decode(menu_custom_data); + + const Object *ob = ED_object_context(C); + const bArmature *arm = static_cast(ob->data); + + /* The "Create a new collection" mode of this operator has its own menu, and should thus be + * invoked. */ + uiLayoutSetOperatorContext(layout, WM_OP_INVOKE_DEFAULT); + uiItemIntO(layout, + "New Bone Collection", + ICON_NONE, + is_move_operation ? "ARMATURE_OT_move_to_collection" : + "ARMATURE_OT_assign_to_collection", + "collection_index", + parent_bcoll_index); + + /* The remaining operators in this menu should be executed on click. Invoking + * them would show this same menu again. */ + uiLayoutSetOperatorContext(layout, WM_OP_EXEC_DEFAULT); + + int child_index, child_count; + if (parent_bcoll_index == -1) { + child_index = 0; + child_count = arm->collection_root_count; + } + else { + /* Add a menu item to assign to the parent first, before listing the children. */ + const BoneCollection *parent = arm->collection_array[parent_bcoll_index]; + menu_add_item_for_move_assign_unassign( + layout, arm, parent, parent_bcoll_index, is_move_operation); + uiItemS(layout); + + child_index = parent->child_index; + child_count = parent->child_count; } - EnumPropertyItem *item = nullptr; - int totitem = 0; - switch (ob->mode) { - case OB_MODE_POSE: { - Object *obpose = ED_pose_object_from_context(C); - if (!obpose) { - return nullptr; - } - bone_collection_enum_itemf_for_object(obpose, &item, &totitem); - break; + /* Loop over the children. There should be at least one, otherwise this parent + * bone collection wouldn't have been drawn as a menu.*/ + for (int index = child_index; index < child_index + child_count; index++) { + const BoneCollection *bcoll = arm->collection_array[index]; + + if (blender::animrig::bonecoll_has_children(bcoll)) { + uiItemMenuF(layout, + bcoll->name, + ICON_NONE, + move_to_collection_menu_create, + menu_custom_data_encode(index, is_move_operation)); + } + else { + menu_add_item_for_move_assign_unassign(layout, arm, bcoll, index, is_move_operation); } - case OB_MODE_EDIT: - bone_collection_enum_itemf_for_object(ob, &item, &totitem); - break; - default: - return rna_enum_dummy_NULL_items; } +} - /* New Collection. */ - EnumPropertyItem item_tmp = {0}; - item_tmp.identifier = "__NEW__"; - item_tmp.name = CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "New Collection"); - item_tmp.value = -1; - RNA_enum_item_add(&item, &totitem, &item_tmp); +static int move_to_collection_regular_invoke(bContext *C, wmOperator *op) +{ + const char *title = CTX_IFACE_(op->type->translation_context, op->type->name); + uiPopupMenu *pup = UI_popup_menu_begin(C, title, ICON_NONE); + uiLayout *layout = UI_popup_menu_layout(pup); - RNA_enum_item_end(&item, &totitem); - *r_free = true; + const bool is_move_operation = STREQ(op->type->idname, "ARMATURE_OT_move_to_collection"); + move_to_collection_menu_create(C, layout, menu_custom_data_encode(-1, is_move_operation)); - return item; + UI_popup_menu_end(C, pup); + + return OPERATOR_INTERFACE; +} + +static int move_to_new_collection_invoke(bContext *C, wmOperator *op) +{ + return WM_operator_props_dialog_popup(C, op, 200); } static int move_to_collection_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) { - PropertyRNA *prop = RNA_struct_find_property(op->ptr, "collection"); + /* Invoking with `collection_index` set has a special meaning: show the menu to create a new bone + * collection as the child of this one. */ + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "collection_index"); if (RNA_property_is_set(op->ptr, prop)) { - const int collection_index = RNA_property_enum_get(op->ptr, prop); - if (collection_index < 0) { - return WM_operator_props_dialog_popup(C, op, 200); - } - /* Either call move_to_collection_exec() or assign_to_collection_exec(), depending on which - * operator got invoked. */ - return op->type->exec(C, op); + return move_to_new_collection_invoke(C, op); } - const char *title = CTX_IFACE_(op->type->translation_context, op->type->name); - uiPopupMenu *pup = UI_popup_menu_begin(C, title, ICON_NONE); - uiLayout *layout = UI_popup_menu_layout(pup); - uiLayoutSetOperatorContext(layout, WM_OP_INVOKE_DEFAULT); - uiItemsEnumO(layout, op->idname, "collection"); - UI_popup_menu_end(C, pup); - return OPERATOR_INTERFACE; + return move_to_collection_regular_invoke(C, op); } void ARMATURE_OT_move_to_collection(wmOperatorType *ot) @@ -1102,24 +1201,28 @@ void ARMATURE_OT_move_to_collection(wmOperatorType *ot) * 'Name' property, without any choice for another collection. */ ot->flag = OPTYPE_UNDO; - prop = RNA_def_enum(ot->srna, - "collection", - rna_enum_dummy_DEFAULT_items, - 0, - "Collection", - "The bone collection to move the selected bones to"); - RNA_def_enum_funcs(prop, bone_collection_enum_itemf); - /* Translation of items is handled by bone_collection_enum_itemf if needed, most are actually - * data (bone collections) names and therefore should not be translated at all. So disable - * automatic translation. */ - RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN | PROP_ENUM_NO_TRANSLATE); + prop = RNA_def_int( + ot->srna, + "collection_index", + -1, + -1, + INT_MAX, + "Collection Index", + "Index of the collection to move selected bones to. When the operator should create a new " + "bone collection, do not include this parameter and pass new_collection_name", + -1, + INT_MAX); + RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN); - prop = RNA_def_string(ot->srna, - "new_collection_name", - nullptr, - MAX_NAME, - "Name", - "Name of the newly added bone collection"); + prop = RNA_def_string( + ot->srna, + "new_collection_name", + nullptr, + MAX_NAME, + "Name", + "Name of a to-be-added bone collection. Only pass this if you want to create a new bone " + "collection and move the selected bones to it. To move to an existing collection, do not " + "include this parameter and use collection_index"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); ot->prop = prop; } @@ -1130,7 +1233,9 @@ void ARMATURE_OT_assign_to_collection(wmOperatorType *ot) /* identifiers */ ot->name = "Assign to Collection"; - ot->description = "Assign bones to a collection"; + ot->description = + "Assign all selected bones to a collection, or unassign them, depending on whether the " + "active bone is already assigned or not"; ot->idname = "ARMATURE_OT_assign_to_collection"; /* api callbacks */ @@ -1144,21 +1249,29 @@ void ARMATURE_OT_assign_to_collection(wmOperatorType *ot) * 'Name' property, without any choice for another collection. */ ot->flag = OPTYPE_UNDO; - prop = RNA_def_enum(ot->srna, - "collection", - rna_enum_dummy_DEFAULT_items, - 0, - "Collection", - "The bone collection to move the selected bones to"); - RNA_def_enum_funcs(prop, bone_collection_enum_itemf); + prop = RNA_def_int( + ot->srna, + "collection_index", + -1, + -1, + INT_MAX, + "Collection Index", + "Index of the collection to assign selected bones to. When the operator should create a new " + "bone collection, use new_collection_name to define the collection name, and set this " + "parameter to the parent index of the new bone collection", + -1, + INT_MAX); RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN); - prop = RNA_def_string(ot->srna, - "new_collection_name", - nullptr, - MAX_NAME, - "Name", - "Name of the newly added bone collection"); + prop = RNA_def_string( + ot->srna, + "new_collection_name", + nullptr, + MAX_NAME, + "Name", + "Name of a to-be-added bone collection. Only pass this if you want to create a new bone " + "collection and assign the selected bones to it. To assign to an existing collection, do " + "not include this parameter and use collection_index"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); ot->prop = prop; } diff --git a/source/blender/editors/include/UI_interface_c.hh b/source/blender/editors/include/UI_interface_c.hh index afd6e644fd6..ae22821ab72 100644 --- a/source/blender/editors/include/UI_interface_c.hh +++ b/source/blender/editors/include/UI_interface_c.hh @@ -2704,6 +2704,8 @@ void uiTemplateLightLinkingCollection(uiLayout *layout, PointerRNA *ptr, const char *propname); +void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C); + #ifdef WITH_GREASE_PENCIL_V3 void uiTemplateGreasePencilLayerTree(uiLayout *layout, bContext *C); #endif diff --git a/source/blender/editors/interface/CMakeLists.txt b/source/blender/editors/interface/CMakeLists.txt index 3dca992a8ce..5a1f98f5aa4 100644 --- a/source/blender/editors/interface/CMakeLists.txt +++ b/source/blender/editors/interface/CMakeLists.txt @@ -67,6 +67,7 @@ set(SRC interface_style.cc interface_template_asset_view.cc interface_template_attribute_search.cc + interface_template_bone_collection_tree.cc interface_template_light_linking.cc interface_template_list.cc interface_template_node_tree_interface.cc diff --git a/source/blender/editors/interface/interface_template_bone_collection_tree.cc b/source/blender/editors/interface/interface_template_bone_collection_tree.cc new file mode 100644 index 00000000000..919cea61a43 --- /dev/null +++ b/source/blender/editors/interface/interface_template_bone_collection_tree.cc @@ -0,0 +1,390 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edinterface + */ + +#include "BKE_context.hh" + +#include "BLT_translation.h" + +#include "ANIM_bone_collections.hh" + +#include "UI_interface.hh" +#include "UI_tree_view.hh" + +#include "RNA_access.hh" +#include "RNA_prototypes.h" + +#include "ED_undo.hh" + +#include "WM_api.hh" + +#include + +namespace blender::ui::bonecollections { + +using namespace blender::animrig; + +class BoneCollectionTreeView : public AbstractTreeView { + protected: + bArmature &armature_; + + public: + explicit BoneCollectionTreeView(bArmature &armature); + void build_tree() override; + + private: + void build_tree_node_recursive(TreeViewItemContainer &parent, const int bcoll_index); +}; + +/** + * Bone collection and the Armature that owns it. + */ +struct ArmatureBoneCollection { + bArmature *armature; + int bcoll_index; + + ArmatureBoneCollection() = default; + ArmatureBoneCollection(bArmature *armature, int bcoll_index) + : armature(armature), bcoll_index(bcoll_index) + { + } + + const BoneCollection &bcoll() const + { + return *armature->collection_array[bcoll_index]; + } + BoneCollection &bcoll() + { + return *armature->collection_array[bcoll_index]; + } +}; + +class BoneCollectionDragController : public AbstractViewItemDragController { + private: + ArmatureBoneCollection drag_arm_bcoll_; + + public: + BoneCollectionDragController(BoneCollectionTreeView &tree_view, + bArmature &armature, + const int bcoll_index); + + eWM_DragDataType get_drag_type() const override; + void *create_drag_data() const override; + void on_drag_start() override; +}; + +class BoneCollectionDropTarget : public TreeViewItemDropTarget { + private: + ArmatureBoneCollection drop_bonecoll_; + + public: + BoneCollectionDropTarget(AbstractTreeViewItem &item, + DropBehavior behavior, + const ArmatureBoneCollection &drop_bonecoll) + : TreeViewItemDropTarget(item, behavior), drop_bonecoll_(drop_bonecoll) + { + } + + bool can_drop(const wmDrag &drag, const char **r_disabled_hint) const override + { + const ArmatureBoneCollection *drag_arm_bcoll = static_cast( + drag.poin); + + /* Do not allow dropping onto another armature. */ + if (drag_arm_bcoll->armature != drop_bonecoll_.armature) { + *r_disabled_hint = "Cannot drag & drop bone collections between Armatures."; + return false; + } + + /* Dragging onto itself doesn't do anything. */ + if (drag_arm_bcoll->bcoll_index == drop_bonecoll_.bcoll_index) { + return false; + } + + /* Do not allow dropping onto its own decendants. */ + if (armature_bonecoll_is_decendent_of( + drag_arm_bcoll->armature, drag_arm_bcoll->bcoll_index, drop_bonecoll_.bcoll_index)) + { + *r_disabled_hint = "Cannot drag a collection onto a descendent"; + return false; + } + + return true; + } + + std::string drop_tooltip(const DragInfo &drag_info) const override + { + const ArmatureBoneCollection *drag_bone_collection = + static_cast(drag_info.drag_data.poin); + const BoneCollection &drag_bcoll = drag_bone_collection->bcoll(); + const BoneCollection &drop_bcoll = drop_bonecoll_.bcoll(); + + std::string_view drag_name = drag_bcoll.name; + std::string_view drop_name = drop_bcoll.name; + + switch (drag_info.drop_location) { + case DropLocation::Into: + return fmt::format(TIP_("Move {} into {}"), drag_name, drop_name); + case DropLocation::Before: + return fmt::format(TIP_("Move {} above {}"), drag_name, drop_name); + case DropLocation::After: + return fmt::format(TIP_("Move {} below {}"), drag_name, drop_name); + } + + return ""; + } + + bool on_drop(bContext *C, const DragInfo &drag_info) const override + { + const ArmatureBoneCollection *drag_arm_bcoll = static_cast( + drag_info.drag_data.poin); + bArmature *arm = drop_bonecoll_.armature; + + const int from_bcoll_index = drag_arm_bcoll->bcoll_index; + const int to_bcoll_index = drop_bonecoll_.bcoll_index; + + int new_bcoll_index = -1; + switch (drag_info.drop_location) { + case DropLocation::Before: + new_bcoll_index = ANIM_armature_bonecoll_move_before_after_index( + arm, from_bcoll_index, to_bcoll_index, MoveLocation::Before); + break; + + case DropLocation::Into: { + if (!ANIM_armature_bonecoll_is_editable(arm, &drop_bonecoll_.bcoll())) { + return false; + } + + const int from_parent_index = armature_bonecoll_find_parent_index(arm, from_bcoll_index); + /* The bone collection becomes the last child of the new parent, as + * that's consistent with the drag & drop of scene collections in the + * outliner. */ + new_bcoll_index = armature_bonecoll_move_to_parent( + arm, from_bcoll_index, -1, from_parent_index, to_bcoll_index); + break; + } + case DropLocation::After: + new_bcoll_index = ANIM_armature_bonecoll_move_before_after_index( + arm, from_bcoll_index, to_bcoll_index, MoveLocation::After); + break; + } + + if (new_bcoll_index < 0) { + return false; + } + + ANIM_armature_bonecoll_active_index_set(arm, new_bcoll_index); + WM_event_add_notifier(C, NC_OBJECT | ND_BONE_COLLECTION, &arm->id); + + ED_undo_push(C, "Reorder Armature Bone Collections"); + return true; + } +}; + +class BoneCollectionItem : public AbstractTreeViewItem { + private: + bArmature &armature_; + int bcoll_index_; + BoneCollection &bone_collection_; + + public: + BoneCollectionItem(bArmature &armature, const int bcoll_index) + : armature_(armature), + bcoll_index_(bcoll_index), + bone_collection_(*armature.collection_array[bcoll_index]) + { + this->label_ = bone_collection_.name; + } + + void build_row(uiLayout &row) override + { + uiLayout *sub = uiLayoutRow(&row, true); + + uiBut *name_label = uiItemL_ex(sub, bone_collection_.name, ICON_NONE, false, false); + if (!ANIM_armature_bonecoll_is_editable(&armature_, &bone_collection_)) { + UI_but_flag_enable(name_label, UI_BUT_INACTIVE); + } + + /* Contains Active Bone icon. */ + /* Performance note: this check potentially loops over all bone collections the active bone is + * assigned to. And this happens for each redraw of each bone collection in the armature. */ + { + const bool contains_active_bone = ANIM_armature_bonecoll_contains_active_bone( + &armature_, &bone_collection_); + const int icon = contains_active_bone ? ICON_DOT : ICON_BLANK1; + uiItemL(sub, "", icon); + } + + /* Visibility eyecon. */ + { + const bool is_visible = bone_collection_.flags & BONE_COLLECTION_VISIBLE; + PointerRNA bcoll_ptr = rna_pointer(); + uiItemR(sub, + &bcoll_ptr, + "is_visible", + UI_ITEM_R_ICON_ONLY, + "", + is_visible ? ICON_HIDE_OFF : ICON_HIDE_ON); + } + } + + void build_context_menu(bContext &C, uiLayout &column) const override + { + MenuType *mt = WM_menutype_find("ARMATURE_MT_collection_tree_context_menu", true); + if (!mt) { + return; + } + UI_menutype_draw(&C, mt, &column); + } + + bool supports_collapsing() const override + { + return true; + } + + std::optional should_be_active() const override + { + return armature_.runtime.active_collection_index == bcoll_index_; + } + + void on_activate(bContext &C) override + { + /* Let RNA handle the property change. This makes sure all the notifiers and DEG + * update calls are properly called. */ + PointerRNA bcolls_ptr = RNA_pointer_create(&armature_.id, &RNA_BoneCollections, &armature_); + PropertyRNA *prop = RNA_struct_find_property(&bcolls_ptr, "active_index"); + + RNA_property_int_set(&bcolls_ptr, prop, bcoll_index_); + RNA_property_update(&const_cast(C), &bcolls_ptr, prop); + + ED_undo_push(&const_cast(C), "Change Armature's Active Bone Collection"); + } + + bool supports_renaming() const override + { + return ANIM_armature_bonecoll_is_editable(&armature_, &bone_collection_); + } + + bool rename(const bContext &C, StringRefNull new_name) override + { + /* Let RNA handle the renaming. This makes sure all the notifiers and DEG + * update calls are properly called. */ + PointerRNA bcoll_ptr = rna_pointer(); + PropertyRNA *prop = RNA_struct_find_property(&bcoll_ptr, "name"); + + RNA_property_string_set(&bcoll_ptr, prop, new_name.c_str()); + RNA_property_update(&const_cast(C), &bcoll_ptr, prop); + + ED_undo_push(&const_cast(C), "Rename Armature Bone Collection"); + return true; + } + + StringRef get_rename_string() const override + { + return bone_collection_.name; + } + + std::unique_ptr create_drag_controller() const override + { + /* Reject dragging linked (or otherwise uneditable) bone collections. */ + if (!ANIM_armature_bonecoll_is_editable(&armature_, &bone_collection_)) { + return {}; + } + + BoneCollectionTreeView &tree_view = static_cast(get_tree_view()); + return std::make_unique(tree_view, armature_, bcoll_index_); + } + + std::unique_ptr create_drop_target() override + { + ArmatureBoneCollection drop_bonecoll(&armature_, bcoll_index_); + /* For now, only support DropBehavior::Insert until there's code for actually reordering + * siblings. Currently only 'move to another parent' is implemented. */ + return std::make_unique( + *this, DropBehavior::ReorderAndInsert, drop_bonecoll); + } + + protected: + /** RNA pointer to the BoneCollection. */ + PointerRNA rna_pointer() + { + return RNA_pointer_create(&armature_.id, &RNA_BoneCollection, &bone_collection_); + } +}; + +BoneCollectionTreeView::BoneCollectionTreeView(bArmature &armature) : armature_(armature) {} + +void BoneCollectionTreeView::build_tree() +{ + for (int bcoll_index = 0; bcoll_index < armature_.collection_root_count; bcoll_index++) { + build_tree_node_recursive(*this, bcoll_index); + } +} + +void BoneCollectionTreeView::build_tree_node_recursive(TreeViewItemContainer &parent, + const int bcoll_index) +{ + BoneCollection *bcoll = armature_.collection_array[bcoll_index]; + BoneCollectionItem &bcoll_tree_item = parent.add_tree_item(armature_, + bcoll_index); + bcoll_tree_item.set_collapsed(false); + + for (int child_index = bcoll->child_index; child_index < bcoll->child_index + bcoll->child_count; + child_index++) + { + build_tree_node_recursive(bcoll_tree_item, child_index); + } +} + +BoneCollectionDragController::BoneCollectionDragController(BoneCollectionTreeView &tree_view, + bArmature &armature, + const int bcoll_index) + : AbstractViewItemDragController(tree_view), drag_arm_bcoll_(&armature, bcoll_index) +{ +} + +eWM_DragDataType BoneCollectionDragController::get_drag_type() const +{ + return WM_DRAG_BONE_COLLECTION; +} + +void *BoneCollectionDragController::create_drag_data() const +{ + ArmatureBoneCollection *drag_data = MEM_new(__func__); + *drag_data = drag_arm_bcoll_; + return drag_data; +} + +void BoneCollectionDragController::on_drag_start() +{ + ANIM_armature_bonecoll_active_index_set(drag_arm_bcoll_.armature, drag_arm_bcoll_.bcoll_index); +} + +} // namespace blender::ui::bonecollections + +void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C) +{ + using namespace blender; + + Object *object = CTX_data_active_object(C); + if (!object || object->type != OB_ARMATURE) { + return; + } + + bArmature *arm = static_cast(object->data); + BLI_assert(GS(arm->id.name) == ID_AR); + + uiBlock *block = uiLayoutGetBlock(layout); + + ui::AbstractTreeView *tree_view = UI_block_add_view( + *block, + "Bone Collection Tree View", + std::make_unique(*arm)); + tree_view->set_min_rows(3); + + ui::TreeViewBuilder::build_tree_view(*tree_view, *layout); +} diff --git a/source/blender/makesdna/DNA_armature_types.h b/source/blender/makesdna/DNA_armature_types.h index b3397d7a490..26db8a41aa1 100644 --- a/source/blender/makesdna/DNA_armature_types.h +++ b/source/blender/makesdna/DNA_armature_types.h @@ -198,7 +198,12 @@ typedef struct bArmature { struct BoneCollection **collection_array; /* Array of `collection_array_num` BoneCollections. */ int collection_array_num; - char _pad2[4]; + /** + * Number of root bone collections. + * + * `collection_array[0:collection_root_count]` are the collections without a parent collection. + */ + int collection_root_count; /** Do not directly assign, use `ANIM_armature_bonecoll_active_set` instead. * This is stored as a string to make it possible for the library overrides system to understand @@ -248,6 +253,15 @@ typedef struct BoneCollection { uint8_t flags; uint8_t _pad0[7]; + /* + * Hierarchy information. The Armature has an array of BoneCollection pointers. These are ordered + * such that siblings are always stored in consecutive array elements. + */ + /** Array index of the first child of this BoneCollection. */ + int child_index; + /** Number of children of this BoneCollection. */ + int child_count; + /** Custom properties. */ struct IDProperty *prop; } BoneCollection; diff --git a/source/blender/makesrna/intern/rna_armature.cc b/source/blender/makesrna/intern/rna_armature.cc index cc5cee9bac8..d9b750a4240 100644 --- a/source/blender/makesrna/intern/rna_armature.cc +++ b/source/blender/makesrna/intern/rna_armature.cc @@ -185,7 +185,8 @@ static void rna_Armature_edit_bone_remove(bArmature *arm, RNA_POINTER_INVALIDATE(ebone_ptr); } -static void rna_iterator_bone_collections_begin(CollectionPropertyIterator *iter, PointerRNA *ptr) +static void rna_iterator_bone_collections_all_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) { bArmature *arm = (bArmature *)ptr->data; rna_iterator_array_begin(iter, @@ -195,13 +196,29 @@ static void rna_iterator_bone_collections_begin(CollectionPropertyIterator *iter false, nullptr); } - -static int rna_iterator_bone_collections_length(PointerRNA *ptr) +static int rna_iterator_bone_collections_all_length(PointerRNA *ptr) { bArmature *arm = (bArmature *)ptr->data; return arm->collection_array_num; } +static void rna_iterator_bone_collections_roots_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + bArmature *arm = (bArmature *)ptr->data; + rna_iterator_array_begin(iter, + arm->collection_array, + sizeof(BoneCollection *), + arm->collection_root_count, + false, + nullptr); +} +static int rna_iterator_bone_collections_roots_length(PointerRNA *ptr) +{ + bArmature *arm = (bArmature *)ptr->data; + return arm->collection_root_count; +} + static void rna_BoneCollections_active_set(PointerRNA *ptr, PointerRNA value, struct ReportList * /*reports*/) @@ -211,6 +228,24 @@ static void rna_BoneCollections_active_set(PointerRNA *ptr, ANIM_armature_bonecoll_active_set(arm, bcoll); } +static void rna_iterator_bone_collection_children_begin(CollectionPropertyIterator *iter, + PointerRNA *ptr) +{ + bArmature *arm = (bArmature *)ptr->owner_id; + const BoneCollection *bcoll = (BoneCollection *)ptr->data; + rna_iterator_array_begin(iter, + arm->collection_array + bcoll->child_index, + sizeof(BoneCollection *), + bcoll->child_count, + false, + nullptr); +} +static int rna_iterator_bone_collection_children_length(PointerRNA *ptr) +{ + const BoneCollection *bcoll = (BoneCollection *)ptr->data; + return bcoll->child_count; +} + static int rna_BoneCollections_active_index_get(PointerRNA *ptr) { bArmature *arm = (bArmature *)ptr->data; @@ -236,6 +271,32 @@ static void rna_BoneCollections_active_index_range( *max = max_ii(0, arm->collection_array_num - 1); } +static BoneCollection *rna_BoneCollections_new(bArmature *armature, + ReportList *reports, + const char *name, + BoneCollection *parent) +{ + if (parent == nullptr) { + BoneCollection *bcoll = ANIM_armature_bonecoll_new(armature, name); + WM_main_add_notifier(NC_OBJECT | ND_BONE_COLLECTION, armature); + return bcoll; + } + + const int32_t parent_index = blender::animrig::armature_bonecoll_find_index(armature, parent); + if (parent_index < 0) { + BKE_reportf(reports, + RPT_ERROR, + "Bone collection '%s' not found in Armature '%s'", + parent->name, + armature->id.name + 2); + return nullptr; + } + + BoneCollection *bcoll = ANIM_armature_bonecoll_new(armature, name, parent_index); + WM_main_add_notifier(NC_OBJECT | ND_BONE_COLLECTION, armature); + return bcoll; +} + static void rna_BoneCollections_active_name_set(PointerRNA *ptr, const char *name) { bArmature *arm = (bArmature *)ptr->data; @@ -270,7 +331,7 @@ static char *rna_BoneCollection_path(const PointerRNA *ptr) char name_esc[sizeof(bcoll->name) * 2]; BLI_str_escape(name_esc, bcoll->name, sizeof(name_esc)); - return BLI_sprintfN("collections[\"%s\"]", name_esc); + return BLI_sprintfN("collections.all[\"%s\"]", name_esc); } static IDProperty **rna_BoneCollection_idprops(PointerRNA *ptr) @@ -343,6 +404,7 @@ static void rna_EditBone_collections_begin(CollectionPropertyIterator *iter, Poi static bool rna_Armature_collections_override_apply(Main *bmain, RNAPropertyOverrideApplyContext &rnaapply_ctx) { + PointerRNA *ptr_src = &rnaapply_ctx.ptr_src; PointerRNA *ptr_dst = &rnaapply_ctx.ptr_dst; PropertyRNA *prop_dst = rnaapply_ctx.prop_dst; PointerRNA *ptr_item_dst = &rnaapply_ctx.ptr_item_dst; @@ -354,11 +416,12 @@ static bool rna_Armature_collections_override_apply(Main *bmain, return false; } + const bArmature *arm_src = (bArmature *)ptr_src->owner_id; bArmature *arm_dst = (bArmature *)ptr_dst->owner_id; BoneCollection *bcoll_anchor = static_cast(ptr_item_dst->data); BoneCollection *bcoll_src = static_cast(ptr_item_src->data); BoneCollection *bcoll = ANIM_armature_bonecoll_insert_copy_after( - arm_dst, bcoll_anchor, bcoll_src); + arm_dst, arm_src, bcoll_anchor, bcoll_src); if (!ID_IS_LINKED(&arm_dst->id)) { /* Mark this bone collection as local override, so that certain operations can be allowed. */ @@ -1814,6 +1877,25 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) RNA_def_struct_ui_text( srna, "Armature Bone Collections", "The Bone Collections of this Armature"); + prop = RNA_def_property(srna, "all", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "BoneCollection"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_bone_collections_all_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_bone_collections_all_length", + nullptr, /* TODO */ + nullptr, /* TODO */ + nullptr); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + /* Bone collections are overridden via the `armature.collections` (the roots). + * It's up to the 'apply' function to also copy the children of a + * library-override-added root. */ + RNA_def_property_override_flag(prop, PROPOVERRIDE_NO_COMPARISON); + RNA_def_property_ui_text( + prop, "Root Bone Collections", "List of the top-level bone collections in the hierarchy"); + prop = RNA_def_property(srna, "active", PROP_POINTER, PROP_NONE); RNA_def_property_struct_type(prop, "BoneCollection"); RNA_def_property_pointer_sdna(prop, nullptr, "runtime.active_collection"); @@ -1827,10 +1909,16 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) RNA_def_property_int_sdna(prop, nullptr, "runtime.active_collection_index"); RNA_def_property_override_flag(prop, PROPOVERRIDE_IGNORE); RNA_def_property_flag(prop, PROP_LIB_EXCEPTION); - RNA_def_property_ui_text(prop, - "Active Collection Index", - "The index of the Armature's active bone collection; -1 when there " - "is no active collection"); + RNA_def_property_ui_text( + prop, + "Active Collection Index", + "The index of the Armature's active bone collection; -1 when there " + "is no active collection. Note that this is indexing the underlying array of bone " + "collections, which may not be in the order you expect. Root collections are listed first, " + "and siblings are always sequential. Apart from that, bone collections can be in any order, " + "and thus incrementing or decrementing this index can make the active bone collection jump " + "around in unexpected ways. For a more predictable interface, use `active` or " + "`active_name`"); RNA_def_property_int_funcs(prop, "rna_BoneCollections_active_index_get", "rna_BoneCollections_active_index_set", @@ -1848,8 +1936,9 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_BoneCollections_active_name_set"); /* Armature.collections.new(...) */ - func = RNA_def_function(srna, "new", "ANIM_armature_bonecoll_new"); + func = RNA_def_function(srna, "new", "rna_BoneCollections_new"); RNA_def_function_ui_description(func, "Add a new empty bone collection to the armature"); + RNA_def_function_flag(func, FUNC_USE_REPORTS); parm = RNA_def_string(func, "name", nullptr, @@ -1858,6 +1947,12 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) "Name of the new collection. Blender will ensure it is unique within the " "collections of the Armature"); RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_pointer( + func, + "parent", + "BoneCollection", + "Parent Collection", + "If not None, the new bone collection becomes a child of this collection"); /* Return value. */ parm = RNA_def_pointer( func, "bonecollection", "BoneCollection", "", "Newly created bone collection"); @@ -1865,7 +1960,11 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) /* Armature.collections.remove(...) */ func = RNA_def_function(srna, "remove", "ANIM_armature_bonecoll_remove"); - RNA_def_function_ui_description(func, "Remove the bone collection from the armature"); + RNA_def_function_ui_description( + func, + "Remove the bone collection from the armature. If this bone collection has any children, " + "they will be reassigned to their grandparent; in other words, the children will take the " + "place of the removed bone collection"); parm = RNA_def_pointer(func, "bone_collection", "BoneCollection", @@ -1875,8 +1974,10 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop) /* Armature.collections.move(...) */ func = RNA_def_function(srna, "move", "rna_BoneCollections_move"); - RNA_def_function_ui_description( - func, "Move a bone collection to a different position in the collection list"); + RNA_def_function_ui_description(func, + "Move a bone collection to a different position in the " + "collection list. This can only be used to reorder siblings, " + "and not to change parent-child relationships"); RNA_def_function_flag(func, FUNC_USE_REPORTS); parm = RNA_def_int( func, "from_index", -1, INT_MIN, INT_MAX, "From Index", "Index to move", 0, 10000); @@ -1971,11 +2072,11 @@ static void rna_def_armature(BlenderRNA *brna) prop = RNA_def_property(srna, "collections", PROP_COLLECTION, PROP_NONE); RNA_def_property_struct_type(prop, "BoneCollection"); RNA_def_property_collection_funcs(prop, - "rna_iterator_bone_collections_begin", + "rna_iterator_bone_collections_roots_begin", "rna_iterator_array_next", "rna_iterator_array_end", "rna_iterator_array_dereference_get", - "rna_iterator_bone_collections_length", + "rna_iterator_bone_collections_roots_length", nullptr, /* TODO */ nullptr, /* TODO */ nullptr); @@ -2125,6 +2226,20 @@ static void rna_def_bonecollection(BlenderRNA *brna) "will always return an empty list of bones, as the bone collection " "memberships are only synchronized when exiting edit mode"); + prop = RNA_def_property(srna, "children", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "BoneCollection"); + RNA_def_property_collection_funcs(prop, + "rna_iterator_bone_collection_children_begin", + "rna_iterator_array_next", + "rna_iterator_array_end", + "rna_iterator_array_dereference_get", + "rna_iterator_bone_collection_children_length", + nullptr, /* TODO */ + nullptr, /* TODO */ + nullptr); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_override_flag(prop, PROPOVERRIDE_NO_COMPARISON); + RNA_api_bonecollection(srna); } diff --git a/source/blender/makesrna/intern/rna_armature_api.cc b/source/blender/makesrna/intern/rna_armature_api.cc index 6ad7b5ceabc..1876d6bdc57 100644 --- a/source/blender/makesrna/intern/rna_armature_api.cc +++ b/source/blender/makesrna/intern/rna_armature_api.cc @@ -171,6 +171,41 @@ static bool rna_BoneCollection_unassign(BoneCollection *bcoll, ANIM_armature_bonecoll_unassign_editbone); } +static int rna_BoneCollection_move_to_parent(ID *owner_id, + BoneCollection *self, + bContext *C, + ReportList *reports, + BoneCollection *to_parent, + const int to_child_num) +{ + using namespace blender::animrig; + + bArmature *armature = (bArmature *)owner_id; + + const int from_bcoll_index = armature_bonecoll_find_index(armature, self); + const int from_parent_index = armature_bonecoll_find_parent_index(armature, from_bcoll_index); + const int to_parent_index = armature_bonecoll_find_index(armature, to_parent); + + if (to_parent_index == from_bcoll_index || + armature_bonecoll_is_decendent_of(armature, from_bcoll_index, to_parent_index)) + { + BKE_report(reports, RPT_ERROR, "Cannot make a bone collection a decendent of itself"); + return from_bcoll_index; + } + + const int new_bcoll_index = armature_bonecoll_move_to_parent( + armature, from_bcoll_index, to_child_num, from_parent_index, to_parent_index); + + WM_event_add_notifier(C, NC_OBJECT | ND_BONE_COLLECTION, nullptr); + return new_bcoll_index; +} + +static int rna_BoneCollection_find_index(ID *owner_id, BoneCollection *self) +{ + bArmature *armature = (bArmature *)owner_id; + return blender::animrig::armature_bonecoll_find_index(armature, self); +} + #else void RNA_api_armature_edit_bone(StructRNA *srna) @@ -320,6 +355,60 @@ void RNA_api_bonecollection(StructRNA *srna) "Whether the bone was actually removed; will be false if the bone was " "not a member of the collection to begin with"); RNA_def_function_return(func, parm); + + /* collection.move_to_parent(parent, child_num) */ + func = RNA_def_function(srna, "move_to_parent", "rna_BoneCollection_move_to_parent"); + RNA_def_function_ui_description( + func, + "Change the hierarchy, by moving this bone collection to become a child of a different " + "parent. Use `parent=None` to make this collection a root collection"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_CONTEXT | FUNC_USE_REPORTS); + + parm = RNA_def_pointer(func, + "to_parent", + "BoneCollection", + "Parent Collection", + "The bone collection becomes a child of this collection. If `None`, the " + "bone collection becomes a root"); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + + parm = RNA_def_int(func, + "to_child_num", + -1, + -1, + INT_MAX, + "Child Number", + "Place the bone collection before this child; `child_num=0` makes it the " + "first child, `child_num=1` the second, etc. Both `child_num=-1` and " + "`child_num=child_count` will move the bone collection to be the last child " + "of the new parent", + -1, + INT_MAX); + + /* Return value. */ + + parm = RNA_def_int(func, + "new_index", + -1, + -1, + INT_MAX, + "New Index", + "Resulting index of the bone collection, after the move. This can be used " + "for manipulating the active bone collection index", + -1, + INT_MAX); + RNA_def_function_return(func, parm); + + /* collection.find_index() */ + func = RNA_def_function(srna, "find_index", "rna_BoneCollection_find_index"); + RNA_def_function_ui_description(func, + "Find the index of this bone collection. This scans through all " + "the Armature's bone collections"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID); + /* Return value. */ + parm = RNA_def_int( + func, "index", -1, -1, INT_MAX, "Index", "Index of the bone collection", -1, INT_MAX); + RNA_def_function_return(func, parm); } #endif diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index b04e1ba5c3b..0d223f525ee 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -2112,6 +2112,10 @@ void RNA_api_ui_layout(StructRNA *srna) RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED); api_ui_item_rna_common(func); + func = RNA_def_function(srna, "template_bone_collection_tree", "uiTemplateBoneCollectionTree"); + RNA_def_function_ui_description(func, "Show bone collections tree"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT); + # ifdef WITH_GREASE_PENCIL_V3 func = RNA_def_function( srna, "template_grease_pencil_layer_tree", "uiTemplateGreasePencilLayerTree"); diff --git a/source/blender/windowmanager/WM_types.hh b/source/blender/windowmanager/WM_types.hh index 0ecd3f2d687..6871fb0369b 100644 --- a/source/blender/windowmanager/WM_types.hh +++ b/source/blender/windowmanager/WM_types.hh @@ -1135,6 +1135,7 @@ enum eWM_DragDataType { WM_DRAG_ASSET_CATALOG, WM_DRAG_GREASE_PENCIL_LAYER, WM_DRAG_NODE_TREE_INTERFACE, + WM_DRAG_BONE_COLLECTION, }; enum eWM_DragFlags {