Anim: make bone collections hierarchical

Make it possible to nest bone collections. The data structure on the
armature is still a flat array. It is organised as follows:

- Sibling collections (i.e. ones with the same parent) are stored
  sequentially in the array.
- Each bone collection keep track of the number of children, and the
  index of the first child.
- Root collections (i.e. ones without parent) are stored as the first
  elements in the array.
- The number of root collections is stored on the Armature.

This commit also contains the following:

- Replaced the flat UIList of bone collections with a tree view.
- Updated the M/Shift+M operators (move/assign to collection) to work
  with hierarchical bone collections.
- Updated RNA interface to expose only root collections at
  `armature.collections`. All collections are available on
  `armature.collections.all`, and children at `bonecollection.children`.
- Library override support. Only new roots + their subtrees can be added
  via overrides.

See https://projects.blender.org/blender/blender/issues/115934

Co-authored with @nathanvegdahl and @nrupsis.

Pull Request: https://projects.blender.org/blender/blender/pulls/115945
This commit is contained in:
Sybren A. Stüvel 2023-12-28 16:10:02 +01:00 committed by Gitea
parent 423ddac25f
commit 129fb2eab8
18 changed files with 2922 additions and 297 deletions

View File

@ -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(

View File

@ -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,

View File

@ -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"

View File

@ -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:
*/

View File

@ -38,6 +38,7 @@ set(SRC
ANIM_keyframing.hh
ANIM_rna.hh
ANIM_visualkey.hh
intern/bone_collections_internal.hh
)
set(LIB

View File

@ -31,6 +31,8 @@
#include "ANIM_armature_iter.hh"
#include "ANIM_bone_collections.hh"
#include "intern/bone_collections_internal.hh"
#include <cstring>
#include <string>
@ -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<BoneCollection *>(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<bArmature *>(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<BoneCollection *, BoneCollection *> 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

View File

@ -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 <stdint.h>
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

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}
}

View File

@ -9,6 +9,8 @@
#include <cstring>
#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<bArmature *>(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<bArmature *>(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<void *>(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<int, bool> menu_custom_data_decode(void *menu_custom_data)
{
*r_free = false;
const uintptr_t index_and_move_bit = reinterpret_cast<intptr_t>(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<bArmature *>(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;
}

View File

@ -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

View File

@ -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

View File

@ -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 <fmt/format.h>
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<const ArmatureBoneCollection *>(
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<const ArmatureBoneCollection *>(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<const ArmatureBoneCollection *>(
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<bool> 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<bContext &>(C), &bcolls_ptr, prop);
ED_undo_push(&const_cast<bContext &>(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<bContext &>(C), &bcoll_ptr, prop);
ED_undo_push(&const_cast<bContext &>(C), "Rename Armature Bone Collection");
return true;
}
StringRef get_rename_string() const override
{
return bone_collection_.name;
}
std::unique_ptr<AbstractViewItemDragController> 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<BoneCollectionTreeView &>(get_tree_view());
return std::make_unique<BoneCollectionDragController>(tree_view, armature_, bcoll_index_);
}
std::unique_ptr<TreeViewItemDropTarget> 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<BoneCollectionDropTarget>(
*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<BoneCollectionItem>(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<ArmatureBoneCollection>(__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<bArmature *>(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<blender::ui::bonecollections::BoneCollectionTreeView>(*arm));
tree_view->set_min_rows(3);
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
}

View File

@ -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;

View File

@ -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<BoneCollection *>(ptr_item_dst->data);
BoneCollection *bcoll_src = static_cast<BoneCollection *>(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);
}

View File

@ -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

View File

@ -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");

View File

@ -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 {