Replace Bone Layers+Groups with Bone Collections #109976

Merged
Nathan Vegdahl merged 7 commits from dr.sybren/blender:anim/armature-collections into main 2023-08-29 14:31:31 +02:00
75 changed files with 3005 additions and 490 deletions

View File

@ -4761,13 +4761,13 @@ def km_pose(params):
("pose.constraints_clear", {"type": 'C', "value": 'PRESS', "ctrl": True, "alt": True}, None),
("pose.ik_add", {"type": 'I', "value": 'PRESS', "shift": True}, None),
("pose.ik_clear", {"type": 'I', "value": 'PRESS', "ctrl": True, "alt": True}, None),
op_menu("VIEW3D_MT_pose_group", {"type": 'G', "value": 'PRESS', "ctrl": True}),
op_menu("VIEW3D_MT_bone_collections", {"type": 'G', "value": 'PRESS', "ctrl": True}),
op_menu("VIEW3D_MT_bone_options_toggle", {"type": 'W', "value": 'PRESS', "shift": True}),
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.layers_show_all", {"type": 'ACCENT_GRAVE', "value": 'PRESS', "ctrl": True}, None),
("armature.armature_layers", {"type": 'M', "value": 'PRESS', "shift": True}, None),
("pose.bone_layers", {"type": 'M', "value": 'PRESS'}, None),
("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_menu", {"type": 'I', "value": 'PRESS'}, None),
("anim.keyframe_delete_v3d", {"type": 'I', "value": 'PRESS', "alt": True}, None),

View File

@ -49,12 +49,6 @@ class DATA_PT_skeleton(ArmatureButtonsPanel, Panel):
layout.row().prop(arm, "pose_position", expand=True)
col = layout.column()
col.label(text="Layers:")
col.prop(arm, "layers", text="")
col.label(text="Protected Layers:")
col.prop(arm, "layers_protected", text="")
class DATA_PT_display(ArmatureButtonsPanel, Panel):
bl_label = "Viewport Display"
@ -88,28 +82,19 @@ class DATA_PT_display(ArmatureButtonsPanel, Panel):
sub.prop(arm, "relation_line_position", text="Relations", expand=True)
class DATA_MT_bone_group_context_menu(Menu):
bl_label = "Bone Group Specials"
class DATA_UL_bone_collections(UIList):
def draw_item(self, _context, layout, armature, bcoll, _icon, _active_data, _active_propname, _index):
active_bone = armature.edit_bones.active or armature.bones.active
has_active_bone = active_bone and bcoll.name in active_bone.collections
def draw(self, _context):
layout = self.layout
layout.operator("pose.group_sort", icon='SORTALPHA')
layout.prop(bcoll, "name", text="", emboss=False,
icon='DOT' if has_active_bone else 'BLANK1')
layout.prop(bcoll, "is_visible", text="", emboss=False,
icon='HIDE_OFF' if bcoll.is_visible else 'HIDE_ON')
class DATA_UL_bone_groups(UIList):
def draw_item(self, _context, layout, _data, item, _icon, _active_data, _active_propname, _index):
layout.prop(item, "name", text="", emboss=False, icon='GROUP_BONE')
if item.is_custom_color_set or item.color_set == 'DEFAULT':
layout.prop(item, "color_set", icon_only=True, icon='COLOR')
else:
layout.prop(item, "color_set", icon_only=True)
class DATA_PT_bone_groups(ArmatureButtonsPanel, Panel):
bl_label = "Bone Groups"
bl_options = {'DEFAULT_CLOSED'}
class DATA_PT_bone_collections(ArmatureButtonsPanel, Panel):
bl_label = "Bone Collections"
@classmethod
def poll(cls, context):
@ -120,59 +105,42 @@ class DATA_PT_bone_groups(ArmatureButtonsPanel, Panel):
layout = self.layout
ob = context.object
pose = ob.pose
group = pose.bone_groups.active
arm = ob.data
active_bcoll = arm.collections.active
row = layout.row()
rows = 1
if group:
if active_bcoll:
rows = 4
row.template_list(
"DATA_UL_bone_groups",
"bone_groups",
pose,
"bone_groups",
pose.bone_groups,
"DATA_UL_bone_collections",
"collections",
arm,
"collections",
arm.collections,
"active_index",
rows=rows,
)
col = row.column(align=True)
col.operator("pose.group_add", icon='ADD', text="")
col.operator("pose.group_remove", icon='REMOVE', text="")
col.menu("DATA_MT_bone_group_context_menu", icon='DOWNARROW_HLT', text="")
if group:
col.operator("armature.collection_add", icon='ADD', text="")
col.operator("armature.collection_remove", icon='REMOVE', text="")
if active_bcoll:
col.separator()
col.operator("pose.group_move", icon='TRIA_UP', text="").direction = 'UP'
col.operator("pose.group_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
if group.is_custom_color_set:
col = layout.column()
split = col.split(factor=0.4)
col = split.column()
row = col.row()
row.alignment = 'RIGHT'
row.label(text="Custom Colors")
col = split.column(align=True)
row = col.row(align=True)
row.prop(group.colors, "normal", text="")
row.prop(group.colors, "select", text="")
row.prop(group.colors, "active", text="")
col.operator("armature.collection_move", icon='TRIA_UP', text="").direction = 'UP'
col.operator("armature.collection_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
row = layout.row()
sub = row.row(align=True)
sub.operator("pose.group_assign", text="Assign")
# row.operator("pose.bone_group_remove_from", text="Remove")
sub.operator("pose.group_unassign", text="Remove")
sub.operator("armature.collection_assign", text="Assign")
sub.operator("armature.collection_unassign", text="Remove")

These should be disabled if there is no active_bcoll. (Just needs to be added to poll.)

These should be disabled if there is no `active_bcoll`. (Just needs to be added to poll.)

When I looked at the actual poll functions, that doesn't actually look like the appropriate place, since these operators can be called with an explicitly specified collection rather than the active one. So it probably makes sense to disable them in the Python code here after all. Same for the other comment about this below.

When I looked at the actual poll functions, that doesn't actually look like the appropriate place, since these operators can be called with an explicitly specified collection rather than the active one. So it probably makes sense to disable them in the Python code here after all. Same for the other comment about this below.
sub = row.row(align=True)
sub.operator("pose.group_select", text="Select")
sub.operator("pose.group_deselect", text="Deselect")
sub.operator("armature.collection_select", text="Select")
sub.operator("armature.collection_deselect", text="Deselect")

These should be disabled if there is no active_bcoll. (Just needs to be added to poll.)

These should be disabled if there is no `active_bcoll`. (Just needs to be added to poll.)
class DATA_PT_iksolver_itasc(ArmatureButtonsPanel, Panel):
@ -278,9 +246,8 @@ class DATA_PT_custom_props_arm(ArmatureButtonsPanel, PropertyPanel, Panel):
classes = (
DATA_PT_context_arm,
DATA_PT_skeleton,
DATA_MT_bone_group_context_menu,
DATA_PT_bone_groups,
DATA_UL_bone_groups,
DATA_PT_bone_collections,
DATA_UL_bone_collections,
DATA_PT_motion_paths,
DATA_PT_motion_paths_display,
DATA_PT_display,

View File

@ -258,7 +258,7 @@ class BONE_PT_display(BoneButtonsPanel, Panel):
@classmethod
def poll(cls, context):
return context.bone
return context.bone or context.edit_bone
def draw(self, context):
# note. this works ok in edit-mode but isn't
@ -266,13 +266,55 @@ class BONE_PT_display(BoneButtonsPanel, Panel):
layout = self.layout
layout.use_property_split = True
bone = context.bone
if bone is None:
bone = context.edit_bone
if context.bone is None:
self.draw_edit_bone(context, layout)
else:
self.draw_bone(context, layout)
if bone:
col = layout.column()
col.prop(bone, "hide", text="Hide", toggle=False)
def draw_bone(self, context, layout):
bone = context.bone
col = layout.column()
col.prop(bone, "hide", text="Hide", toggle=False)
# Figure out the pose bone.
ob = context.object
if not ob:
return
pose_bone = ob.pose.bones[bone.name]
layout.prop(bone.color, 'palette', text='Edit Bone Color')
self.draw_bone_color_ui(layout, bone.color)
layout.prop(pose_bone.color, 'palette', text='Pose Bone Color')
self.draw_bone_color_ui(layout, pose_bone.color)
def draw_edit_bone(self, context, layout):
bone = context.edit_bone
if bone is None:
return
col = layout.column()
col.prop(bone, "hide", text="Hide", toggle=False)
layout.prop(bone.color, 'palette', text='Edit Bone Color')
self.draw_bone_color_ui(layout, bone.color)
def draw_bone_color_ui(self, layout, bone_color):
if not bone_color.is_custom:
return
layout.use_property_split = False
split = layout.split(factor=0.4)
col = split.column()
row = col.row()
row.alignment = 'RIGHT'
row.label(text="Custom Colors")
col = split.column(align=True)
row = col.row(align=True)
row.prop(bone_color.custom, "normal", text="")
row.prop(bone_color.custom, "select", text="")
row.prop(bone_color.custom, "active", text="")
class BONE_PT_display_custom_shape(BoneButtonsPanel, Panel):

View File

@ -3815,7 +3815,7 @@ class VIEW3D_MT_pose(Menu):
layout.separator()
layout.menu("VIEW3D_MT_pose_motion")
layout.menu("VIEW3D_MT_pose_group")
layout.menu("VIEW3D_MT_bone_collections")
layout.separator()
@ -3900,26 +3900,35 @@ class VIEW3D_MT_pose_motion(Menu):
layout.operator("pose.paths_clear", text="Clear")
class VIEW3D_MT_pose_group(Menu):
bl_label = "Bone Groups"
class VIEW3D_MT_bone_collections(Menu):
bl_label = "Bone Collections"
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.type == 'ARMATURE'
def draw(self, context):
layout = self.layout
pose = context.active_object.pose
props = layout.operator("armature.collection_assign",
text="Assign to New Collection")
props.name = "New Collection"
layout.operator_context = 'EXEC_AREA'
layout.operator("pose.group_assign", text="Assign to New Group").type = 0
arm = context.active_object.data
if not arm.collections.active:
return
if pose.bone_groups:
active_group = pose.bone_groups.active_index + 1
layout.operator("pose.group_assign", text="Assign to Group").type = active_group
layout.separator()
layout.separator()
layout.operator("armature.collection_assign",
text="Assign to '%s'" % arm.collections.active.name)
layout.operator("armature.collection_unassign",
text="Unassign from '%s'" % arm.collections.active.name)
# layout.operator_context = 'INVOKE_AREA'
layout.operator("pose.group_unassign")
layout.operator("pose.group_remove")
layout.separator()
layout.operator("armature.collection_remove",
text="Remove Collection '%s'" % arm.collections.active.name)
class VIEW3D_MT_pose_ik(Menu):
@ -8441,7 +8450,7 @@ classes = (
VIEW3D_MT_pose_slide,
VIEW3D_MT_pose_propagate,
VIEW3D_MT_pose_motion,
VIEW3D_MT_pose_group,
VIEW3D_MT_bone_collections,
VIEW3D_MT_pose_ik,
VIEW3D_MT_pose_constraints,
VIEW3D_MT_pose_names,

View File

@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief Iterators for armatures.
*/
#pragma once
#ifndef __cplusplus
# error This is a C++ header.
#endif
#include "DNA_armature_types.h"
#include "BLI_listbase_wrapper.hh"
namespace blender::animrig {
/**
* Call `callback(bone)` for each bone in the list of bones.
*
* Bones are visited in depth-first order.
*
* TODO: extend the callback with a `bool` return value to indicate whether the
* loop should continue or stop.
*/
template<typename CB>
static void ANIM_armature_foreach_bone(ListBase /* Bone */ *bones, CB callback)
{
for (Bone *bone : blender::ListBaseWrapper<Bone>(bones)) {
callback(bone);
ANIM_armature_foreach_bone(&bone->childbase, callback);
}
}
/**
* Call `callback(bone)` for each bone in the list of bones.
*
* Bones are visited in depth-first order.
*
* TODO: extend the callback with a `bool` return value to indicate whether the
* loop should continue or stop.
*/
template<typename CB>
static void ANIM_armature_foreach_bone(const ListBase /* Bone */ *bones, CB callback)
{
for (const Bone *bone : blender::ConstListBaseWrapper<Bone>(bones)) {
callback(bone);
ANIM_armature_foreach_bone(&bone->childbase, callback);
}
}
}; // namespace blender::animrig

View File

@ -25,10 +25,121 @@ extern "C" {
struct bArmature;
struct Bone;
struct BoneCollection;
struct bPoseChannel;
struct EditBone;
/**
* Construct a new #BoneCollection with the given name.
*
* The caller owns the returned pointer.
*
* You don't typically use this function directly, but rather create a bone collection on a
* bArmature.
*
* \see #ANIM_armature_bonecoll_new
*/
struct BoneCollection *ANIM_bonecoll_new(const char *name) ATTR_WARN_UNUSED_RESULT;
/**
* Free the bone collection.
*
* You don't typically need this function, unless you created a bone collection outside the scope
* of a bArmature. Normally bone collections are owned (and thus managed) by the armature.
*
* \see ANIM_armature_bonecoll_remove
*/
void ANIM_bonecoll_free(struct BoneCollection *bcoll);
/**
* Recalculate the armature & bone runtime data.
*
* NOTE: this should only be used when the runtime structs on the Armature and Bones are still
* empty. Any data allocated there will NOT be freed.
*
* TODO: move to BKE?
*/
void ANIM_armature_runtime_refresh(struct bArmature *armature);
/**
* Free armature & bone runtime data.
* TODO: move to BKE?
*/
void ANIM_armature_runtime_free(struct bArmature *armature);
/**
* Add a new bone collection to the given armature.
*
* The Armature owns the returned pointer.
*/
struct BoneCollection *ANIM_armature_bonecoll_new(struct bArmature *armature, const char *name);
/**
* Remove a bone collection from the armature.
*/
void ANIM_armature_bonecoll_remove(struct bArmature *armature, struct BoneCollection *bcoll);
/**
* Set the given bone collection as the active one.
*
* Pass `nullptr` to clear the active bone collection.
*/
void ANIM_armature_bonecoll_active_set(struct bArmature *armature, struct BoneCollection *bcoll);
/**
* Set the bone collection with the given index as the active one.
*
* Pass -1 to clear the active bone collection.
*/
void ANIM_armature_bonecoll_active_index_set(struct bArmature *armature,
int bone_collection_index);
/**
* Move the bone collection by \a step places up/down.
*
* \return whether the move actually happened.
*/
bool ANIM_armature_bonecoll_move(struct bArmature *armature,
struct BoneCollection *bcoll,
int step);
struct BoneCollection *ANIM_armature_bonecoll_get_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);
void ANIM_bonecoll_hide(struct BoneCollection *bcoll);
/**
* Assign the bone to the bone collection.
*
* No-op if the bone is already a member of the collection.
*
* \return true if the bone was actually assigned, false if not (f.e. when it already was assigned
* previously).
*/
bool ANIM_armature_bonecoll_assign(struct BoneCollection *bcoll, struct Bone *bone);
bool ANIM_armature_bonecoll_assign_editbone(struct BoneCollection *bcoll, struct EditBone *ebone);
bool ANIM_armature_bonecoll_assign_and_move(struct BoneCollection *bcoll, struct Bone *bone);
bool ANIM_armature_bonecoll_unassign(struct BoneCollection *bcoll, struct Bone *bone);
bool ANIM_armature_bonecoll_unassign_editbone(struct BoneCollection *bcoll,
struct EditBone *ebone);
/* Assign the edit bone to the armature's active collection. */
void ANIM_armature_bonecoll_assign_active(const struct bArmature *armature,
struct EditBone *ebone);
/**
* Reconstruct the bone collection memberships, based on the bone runtime data.
*
* This is needed to transition out of armature edit mode. That removes all bones, and
* recreates them from the editbones.
*/
void ANIM_armature_bonecoll_reconstruct(struct bArmature *armature);
/*
* Armature/Bone Layer abstractions. These functions are intended as the sole
* accessors for `bone->layer`, `armature->layer`, etc. to get a grip on which
* queries & operations are performed.
@ -38,10 +149,8 @@ struct EditBone;
* first step towards replacement.
*/
inline bool ANIM_bonecoll_is_visible(const struct bArmature *armature, const struct Bone *bone)
{
return armature->layer & bone->layer;
}
/** Return true when any of the bone's collections is visible. */
bool ANIM_bonecoll_is_visible(const struct bArmature *armature, const struct Bone *bone);
inline bool ANIM_bone_is_visible(const struct bArmature *armature, const struct Bone *bone)
{
@ -49,10 +158,14 @@ inline bool ANIM_bone_is_visible(const struct bArmature *armature, const struct
return bone_itself_visible && ANIM_bonecoll_is_visible(armature, bone);
}
inline bool ANIM_bonecoll_is_visible_editbone(const struct bArmature *armature,
const struct EditBone *ebone)
bool ANIM_bonecoll_is_visible_editbone(const struct bArmature *armature,
const struct EditBone *ebone);
inline bool ANIM_bone_is_visible_editbone(const struct bArmature *armature,
const struct EditBone *ebone)
{
return armature->layer & ebone->layer;
const bool bone_itself_visible = (ebone->flag & BONE_HIDDEN_A) == 0;
return bone_itself_visible && ANIM_bonecoll_is_visible_editbone(armature, ebone);
}
inline bool ANIM_bonecoll_is_visible_pchan(const struct bArmature *armature,
@ -66,18 +179,18 @@ inline bool ANIM_bonecoll_is_visible_actbone(const struct bArmature *armature)
return ANIM_bonecoll_is_visible(armature, armature->act_bone);
}
void ANIM_armature_bonecoll_show_all(struct bArmature *armature);
void ANIM_armature_bonecoll_hide_all(struct bArmature *armature);
/* Only used by the Collada I/O code: */
void ANIM_armature_enable_layers(struct bArmature *armature, const int layers);
void ANIM_armature_disable_all_layers(struct bArmature *armature);
void ANIM_bone_set_layer_ebone(struct EditBone *ebone, int layer);
void ANIM_bone_set_ebone_layer_from_armature(struct EditBone *ebone,
const struct bArmature *armature);
void ANIM_armature_ensure_first_layer_enabled(struct bArmature *armature);
void ANIM_armature_ensure_layer_enabled_from_bone(struct bArmature *armature,
const struct Bone *bone);
void ANIM_armature_ensure_layer_enabled_from_ebone(struct bArmature *armature,
const struct EditBone *ebone);
void ANIM_armature_ensure_layer_enabled_from_pchan(struct bArmature *armature,
const struct bPoseChannel *pchan);
void ANIM_armature_bonecoll_show_from_bone(struct bArmature *armature, const struct Bone *bone);
void ANIM_armature_bonecoll_show_from_ebone(struct bArmature *armature,
const struct EditBone *ebone);
void ANIM_armature_bonecoll_show_from_pchan(struct bArmature *armature,
const struct bPoseChannel *pchan);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*
* \brief C++ part of the BoneColor DNA struct.
*/
#pragma once
#ifndef __cplusplus
# error This is a C++ header.
#endif
#include "DNA_anim_types.h"
struct ThemeWireColor;
nathanvegdahl marked this conversation as resolved Outdated

It looks like this is defined in DNA_userdef_types.h. I'd be more comfortable just including that instead to make it explicit where ThemeWireColor is coming from. I guess the concern here is bloating build times?

It looks like this is defined in `DNA_userdef_types.h`. I'd be more comfortable just including that instead to make it explicit where `ThemeWireColor` is coming from. I guess the concern here is bloating build times?
namespace blender::animrig {
/** C++ wrapper for the DNA BoneColor struct. */
class BoneColor : public ::BoneColor {
public:
BoneColor();
BoneColor(const BoneColor &other);
~BoneColor();
const ThemeWireColor *effective_color() const;
};
}; // namespace blender::animrig

View File

@ -7,6 +7,7 @@ set(INC
intern
../blenkernel
../editors/include
)
set(INC_SYS
@ -14,26 +15,31 @@ set(INC_SYS
set(SRC
intern/bone_collections.cc
intern/bonecolor.cc
ANIM_bone_collections.h
ANIM_bonecolor.hh
)
set(LIB
bf_blenkernel
bf::blenlib
bf::dna
PRIVATE bf_editor_interface
PRIVATE bf::intern::guardedalloc
)
blender_add_lib(bf_animrig "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_library(bf::animrig ALIAS bf_animrig)
# if(WITH_GTESTS)
# set(TEST_SRC
# )
# set(TEST_LIB
# PRIVATE bf::animrig
# )
# include(GTestTesting)
# blender_add_test_lib(bf_animrig_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
# endif()
if(WITH_GTESTS)
set(TEST_SRC
intern/bone_collections_test.cc
)
set(TEST_LIB
PRIVATE bf::animrig
)
include(GTestTesting)
blender_add_test_lib(bf_animrig_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@ -6,57 +6,426 @@
* \ingroup animrig
*/
#include "BLI_linklist.h"
#include "BLI_math_color.h"
#include "BLI_string.h"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
#include "DNA_armature_types.h"
#include "BLI_math_bits.h"
#include "MEM_guardedalloc.h"
#include "BKE_animsys.h"
#include "ANIM_armature_iter.hh"
#include "ANIM_bone_collections.h"
#include <cstring>
#include <string>
using std::strcmp;
using namespace blender::animrig;
namespace {
/** Default flags for new bone collections. */
constexpr eBoneCollection_Flag default_flags = BONE_COLLECTION_VISIBLE |
BONE_COLLECTION_SELECTABLE;
} // namespace
BoneCollection *ANIM_bonecoll_new(const char *name)
{
if (name == nullptr || name[0] == '\0') {
/* Use a default name if no name was given. */
name = "Bones";
}
/* Note: the collection name may change after the collection is added to an
* armature, to ensure it is unique within the armature. */
std::string alloc_name = std::string(__func__) + "('" + name + "')";
BoneCollection *bcoll = MEM_cnew<BoneCollection>(alloc_name.c_str());
BLI_strncpy(bcoll->name, name, sizeof(bcoll->name));
bcoll->flags = default_flags;
bcoll->prop = nullptr;
return bcoll;
}
void ANIM_bonecoll_free(BoneCollection *bcoll)
{
BLI_assert_msg(BLI_listbase_is_empty(&bcoll->bones),
"bone collection still has bones assigned to it, will cause dangling pointers in "
"bone runtime data");
MEM_delete(bcoll);
}
void ANIM_armature_runtime_refresh(bArmature *armature)
{
ANIM_armature_runtime_free(armature);
ANIM_armature_bonecoll_active_set(armature, armature->active_collection);
/* Construct the bone-to-collections mapping. */
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) {
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&member->bone->runtime.collections, ref);
}
}
}
void ANIM_armature_runtime_free(bArmature *armature)
{
/* Free the bone-to-its-collections mapping. */
ANIM_armature_foreach_bone(&armature->bonebase,
[&](Bone *bone) { BLI_freelistN(&bone->runtime.collections); });
}
static void bonecoll_ensure_name_unique(bArmature *armature, BoneCollection *bcoll)
{
BLI_uniquename(&armature->collections,
bcoll,
bcoll->name,
'.',
offsetof(BoneCollection, name),
sizeof(bcoll->name));
}
BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name)
{
BoneCollection *bcoll = ANIM_bonecoll_new(name);
bonecoll_ensure_name_unique(armature, bcoll);
BLI_addtail(&armature->collections, bcoll);
return bcoll;
}
static void armature_bonecoll_active_clear(bArmature *armature)
{
armature->runtime.active_collection_index = -1;
armature->active_collection = nullptr;
}
void ANIM_armature_bonecoll_active_set(bArmature *armature, BoneCollection *bcoll)
{
if (bcoll == nullptr) {
armature_bonecoll_active_clear(armature);
return;
}
const int index = BLI_findindex(&armature->collections, bcoll);
if (index == -1) {
/* TODO: print warning? Or just ignore this case? */
armature_bonecoll_active_clear(armature);
return;
}
armature->runtime.active_collection_index = index;
armature->active_collection = bcoll;
}
void ANIM_armature_bonecoll_active_index_set(bArmature *armature, const int bone_collection_index)
{
if (bone_collection_index < 0) {
armature_bonecoll_active_clear(armature);
return;
}
void *found_link = BLI_findlink(&armature->collections, bone_collection_index);
BoneCollection *bcoll = static_cast<BoneCollection *>(found_link);
if (bcoll == nullptr) {
/* TODO: print warning? Or just ignore this case? */
armature_bonecoll_active_clear(armature);
return;
}
armature->runtime.active_collection_index = bone_collection_index;
armature->active_collection = bcoll;
}
bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, const int step)
{
if (bcoll == NULL) {
return false;
}
if (!BLI_listbase_link_move(&armature->collections, bcoll, step)) {
return false;
}
if (bcoll == armature->active_collection) {
armature->runtime.active_collection_index = BLI_findindex(&armature->collections, bcoll);
}
return true;
}
void ANIM_armature_bonecoll_name_set(bArmature *armature, BoneCollection *bcoll, const char *name)
{
char old_name[sizeof(bcoll->name)];
BLI_strncpy(old_name, bcoll->name, sizeof(bcoll->name));
BLI_strncpy(bcoll->name, name, sizeof(bcoll->name));
bonecoll_ensure_name_unique(armature, bcoll);
BKE_animdata_fix_paths_rename_all(&armature->id, "collections", old_name, bcoll->name);
}
void ANIM_armature_bonecoll_remove(bArmature *armature, BoneCollection *bcoll)
{
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);
}
}
BLI_remlink_safe(&armature->collections, bcoll);
ANIM_bonecoll_free(bcoll);
/* Make sure the active collection is correct. */
const int num_collections = BLI_listbase_count(&armature->collections);
const int active_index = min_ii(armature->runtime.active_collection_index, num_collections - 1);
ANIM_armature_bonecoll_active_index_set(armature, active_index);
}
BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature, const char *name)
{
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
if (STREQ(bcoll->name, name)) {
return bcoll;
}
}
return nullptr;
}
void ANIM_bonecoll_hide(BoneCollection *bcoll)
{
bcoll->flags &= ~BONE_COLLECTION_VISIBLE;
}
/* Store the bone's membership on the collection. */
static void add_membership(BoneCollection *bcoll, Bone *bone)
{
BoneCollectionMember *member = MEM_cnew<BoneCollectionMember>(__func__);
member->bone = bone;
BLI_addtail(&bcoll->bones, member);
}
bool ANIM_armature_bonecoll_assign(BoneCollection *bcoll, Bone *bone)
{
/* Precondition check: bail out if already a member. */
LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) {
if (member->bone == bone) {
return false;
}
}
add_membership(bcoll, bone);
/* Store reverse membership on the bone. */
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&bone->runtime.collections, ref);
return true;
}
bool ANIM_armature_bonecoll_assign_editbone(BoneCollection *bcoll, EditBone *ebone)
{
/* Precondition check: bail out if already a member. */
LISTBASE_FOREACH (BoneCollectionReference *, ref, &ebone->bone_collections) {
if (ref->bcoll == bcoll) {
return false;
}
}
/* Store membership on the edit bone. Bones will be rebuilt when the armature
* goes out of edit mode, and by then the newly created bones will be added to
* the actual collection on the Armature. */
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&ebone->bone_collections, ref);
return true;
}
bool ANIM_armature_bonecoll_assign_and_move(BoneCollection *bcoll, Bone *bone)
{
/* Remove the bone from all its current collections. */
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &bone->runtime.collections) {
ANIM_armature_bonecoll_unassign(ref->bcoll, bone);
}
/* Assign the new collection. */
return ANIM_armature_bonecoll_assign(bcoll, bone);
}
bool ANIM_armature_bonecoll_unassign(BoneCollection *bcoll, Bone *bone)
{
bool was_found = false;
/* Remove membersip from collection. */
LISTBASE_FOREACH_MUTABLE (BoneCollectionMember *, member, &bcoll->bones) {
if (member->bone == bone) {
BLI_freelinkN(&bcoll->bones, member);
was_found = true;
break;
}
}
/* Remove reverse membership from the bone.
* For data consistency sake, this is always done, regardless of whether the
* above loop found the membership. */
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &bone->runtime.collections) {
if (ref->bcoll == bcoll) {
BLI_freelinkN(&bone->runtime.collections, ref);
break;
}
}
return was_found;
}
bool ANIM_armature_bonecoll_unassign_editbone(BoneCollection *bcoll, EditBone *ebone)
{
bool was_found = false;
/* Edit bone membership is only stored on the edit bone itself. */
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &ebone->bone_collections) {
if (ref->bcoll == bcoll) {
BLI_freelinkN(&ebone->bone_collections, ref);
was_found = true;
break;
}
}
return was_found;
}
void ANIM_armature_bonecoll_reconstruct(struct bArmature *armature)
{
/* Remove all the old collection memberships. */
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
BLI_freelistN(&bcoll->bones);
}
/* For all bones, restore their collection memberships. */
ANIM_armature_foreach_bone(&armature->bonebase, [&](Bone *bone) {
LISTBASE_FOREACH (BoneCollectionReference *, ref, &bone->runtime.collections) {
add_membership(ref->bcoll, bone);
}
});
}
static bool any_bone_collection_visible(const ListBase /*BoneCollectionRef*/ *collection_refs)
{
/* Special case: when a bone is not in any collection, it is visible. */
if (BLI_listbase_is_empty(collection_refs)) {
return true;
}
LISTBASE_FOREACH (const BoneCollectionReference *, bcoll_ref, collection_refs) {
const BoneCollection *bcoll = bcoll_ref->bcoll;
if (bcoll->flags & BONE_COLLECTION_VISIBLE) {
return true;
}
}
return false;
}
/* TODO: these two functions were originally implemented for armature layers, hence the armature
* parameters. These should be removed at some point. */
bool ANIM_bonecoll_is_visible(const struct bArmature * /*armature*/, const struct Bone *bone)
{
return any_bone_collection_visible(&bone->runtime.collections);
}
bool ANIM_bonecoll_is_visible_editbone(const struct bArmature * /*armature*/,
const struct EditBone *ebone)
{
return any_bone_collection_visible(&ebone->bone_collections);
}
void ANIM_armature_bonecoll_show_all(bArmature *armature)
{
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
bcoll->flags |= BONE_COLLECTION_VISIBLE;
}
}
void ANIM_armature_bonecoll_hide_all(bArmature *armature)
{
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
bcoll->flags &= ~BONE_COLLECTION_VISIBLE;
}
}
/* ********************************* */
/* Armature Layers transitional API. */
void ANIM_armature_enable_layers(bArmature *armature, const int layers)
void ANIM_armature_enable_layers(bArmature *armature, const int /*layers*/)
{
armature->layer |= layers;
}
void ANIM_armature_disable_all_layers(bArmature *armature)
{
armature->layer = 0;
// TODO: reimplement properly.
// armature->layer |= layers;
ANIM_armature_bonecoll_show_all(armature);
}
void ANIM_bone_set_layer_ebone(EditBone *ebone, const int layer)
{
// TODO: reimplement for bone collections.
ebone->layer = layer;
}
void ANIM_bone_set_ebone_layer_from_armature(EditBone *ebone, const bArmature *armature)
void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *ebone)
{
ebone->layer = armature->layer;
if (armature->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->active_collection, ebone);
}
void ANIM_armature_ensure_first_layer_enabled(bArmature *armature)
{
armature->layer = 1;
}
void ANIM_armature_ensure_layer_enabled_from_bone(bArmature *armature, const Bone *bone)
void ANIM_armature_bonecoll_show_from_bone(bArmature *armature, const Bone *bone)
{
if (ANIM_bonecoll_is_visible(armature, bone)) {
return;
}
armature->layer |= 1U << bitscan_forward_uint(bone->layer);
/* Making the first collection visible is enough to make the bone visible.
*
* Since bones without collection are considered visible,
* bone->runtime.collections.first is certainly a valid pointer. */
BoneCollectionReference *ref = static_cast<BoneCollectionReference *>(
bone->runtime.collections.first);
ref->bcoll->flags |= BONE_COLLECTION_VISIBLE;
}
void ANIM_armature_ensure_layer_enabled_from_ebone(bArmature *armature, const EditBone *ebone)
void ANIM_armature_bonecoll_show_from_ebone(bArmature *armature, const EditBone *ebone)
{
if (ANIM_bonecoll_is_visible_editbone(armature, ebone)) {
return;
}
armature->layer |= 1U << bitscan_forward_uint(ebone->layer);
/* Making the first collection visible is enough to make the bone visible.
*
* Since bones without collection are considered visible,
* ebone->bone_collections.first is certainly a valid pointer. */
BoneCollectionReference *ref = static_cast<BoneCollectionReference *>(
ebone->bone_collections.first);
ref->bcoll->flags |= BONE_COLLECTION_VISIBLE;
}
void ANIM_armature_ensure_layer_enabled_from_pchan(bArmature *armature, const bPoseChannel *pchan)
void ANIM_armature_bonecoll_show_from_pchan(bArmature *armature, const bPoseChannel *pchan)
{
ANIM_armature_ensure_layer_enabled_from_bone(armature, pchan->bone);
ANIM_armature_bonecoll_show_from_bone(armature, pchan->bone);
}

View File

@ -0,0 +1,115 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_string.h"
#include "ANIM_bone_collections.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
TEST(ANIM_bone_collections, bonecoll_new_free)
{
BoneCollection *bcoll = ANIM_bonecoll_new("some name");
EXPECT_NE(nullptr, bcoll);
EXPECT_EQ("some name", std::string(bcoll->name));
EXPECT_TRUE(BLI_listbase_is_empty(&bcoll->bones));
EXPECT_EQ(BONE_COLLECTION_VISIBLE | BONE_COLLECTION_SELECTABLE, bcoll->flags);
ANIM_bonecoll_free(bcoll);
}
TEST(ANIM_bone_collections, bonecoll_default_name)
{
{
BoneCollection *bcoll = ANIM_bonecoll_new("");
EXPECT_EQ("Bones", std::string(bcoll->name));
ANIM_bonecoll_free(bcoll);
}
{
BoneCollection *bcoll = ANIM_bonecoll_new(nullptr);
EXPECT_EQ("Bones", std::string(bcoll->name));
ANIM_bonecoll_free(bcoll);
}
}
class ANIM_armature_bone_collections : public testing::Test {
protected:
bArmature arm;
Bone bone1, bone2, bone3;
void SetUp() override
{
memset(&arm, 0, sizeof(arm));
memset(&bone1, 0, sizeof(Bone));
memset(&bone2, 0, sizeof(Bone));
memset(&bone3, 0, sizeof(Bone));
STRNCPY(bone1.name, "bone1");
STRNCPY(bone2.name, "bone2");
STRNCPY(bone3.name, "bone3");
BLI_addtail(&arm.bonebase, &bone1); /* bone1 is root bone. */
BLI_addtail(&arm.bonebase, &bone2); /* bone2 is root bone. */
BLI_addtail(&bone2.childbase, &bone3); /* bone3 has bone2 as parent. */
}
};
TEST_F(ANIM_armature_bone_collections, armature_owned_collections)
{
BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "collection");
BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "collection");
EXPECT_EQ(std::string("collection"), std::string(bcoll1->name));
EXPECT_EQ(std::string("collection.001"), std::string(bcoll2->name));
ANIM_armature_bonecoll_remove(&arm, bcoll1);
ANIM_armature_bonecoll_remove(&arm, bcoll2);
}
TEST_F(ANIM_armature_bone_collections, bones_assign_unassign)
{
BoneCollection *bcoll = ANIM_armature_bonecoll_new(&arm, "collection");
ANIM_armature_bonecoll_assign(bcoll, &bone1);
ANIM_armature_bonecoll_assign(bcoll, &bone2);
ASSERT_EQ(2, BLI_listbase_count(&bcoll->bones)) << "expecting two bones in collection";
EXPECT_EQ(&bone1, static_cast<BoneCollectionMember *>(BLI_findlink(&bcoll->bones, 0))->bone);
EXPECT_EQ(&bone2, static_cast<BoneCollectionMember *>(BLI_findlink(&bcoll->bones, 1))->bone);
EXPECT_EQ(bcoll, static_cast<BoneCollectionReference *>(bone1.runtime.collections.first)->bcoll)
<< "expecting back-reference to collection in bone1 runtime data";
EXPECT_EQ(bcoll, static_cast<BoneCollectionReference *>(bone2.runtime.collections.first)->bcoll)
<< "expecting back-reference to collection in bone2 runtime data";
ANIM_armature_bonecoll_unassign(bcoll, &bone1);
ANIM_armature_bonecoll_unassign(bcoll, &bone2);
EXPECT_EQ(0, BLI_listbase_count(&bone1.runtime.collections))
<< "expecting back-references in bone1 runtime data to be cleared when unassigned";
EXPECT_EQ(0, BLI_listbase_count(&bone2.runtime.collections))
<< "expecting back-references in bone2 runtime data to be cleared when unassigned";
ANIM_armature_bonecoll_remove(&arm, bcoll);
}
TEST_F(ANIM_armature_bone_collections, bones_assign_remove)
{
BoneCollection *bcoll = ANIM_armature_bonecoll_new(&arm, "collection");
ANIM_armature_bonecoll_assign(bcoll, &bone1);
ANIM_armature_bonecoll_assign(bcoll, &bone2);
ANIM_armature_bonecoll_remove(&arm, bcoll);
EXPECT_EQ(0, BLI_listbase_count(&bone1.runtime.collections))
<< "expecting back-references in bone1 runtime data to be cleared when the collection is "
"removed";
EXPECT_EQ(0, BLI_listbase_count(&bone2.runtime.collections))
<< "expecting back-references in bone2 runtime data to be cleared when the collection is "
"removed";
}
} // namespace blender::animrig::tests

View File

@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "ANIM_bonecolor.hh"
#include "UI_resources.hh"
#include <cstring>
namespace blender::animrig {
BoneColor::BoneColor()
{
this->palette_index = 0;
}
BoneColor::BoneColor(const BoneColor &other)
{
this->palette_index = other.palette_index;
std::memcpy(&this->custom, &other.custom, sizeof(this->custom));
}
BoneColor::~BoneColor() {}
const ThemeWireColor *BoneColor::effective_color() const
{
const int8_t color_index = this->palette_index;
if (color_index == 0) {
return nullptr;
}
if (color_index < 0) {
return &this->custom;
}
const bTheme *btheme = UI_GetTheme();
return &btheme->tarm[(color_index - 1)];
}
}; // namespace blender::animrig

View File

@ -24,6 +24,7 @@ struct bArmature;
/* The following structures are defined in DNA_action_types.h, and DNA_anim_types.h */
struct AnimationEvalContext;
struct BoneColor;
struct FCurve;
struct ID;
struct Main;
@ -125,6 +126,19 @@ void set_active_action_group(struct bAction *act, struct bActionGroup *agrp, sho
*/
void action_group_colors_sync(struct bActionGroup *grp, const struct bActionGroup *ref_grp