Files
test/source/blender/animrig/intern/bone_collections.cc
Damien Picard 730bb2ee3e I18n: make new bones and bone collections' names translatable
Newly created bones and bone collections get a default name. Like
other types of data, these names should be translated if the user
enabled the translation of new data in the preferences.

This commit adds the appropriate `DATA_()` macro:
- when creating a new armature;
- when creating a new bone;
- when creating a new bone collection through `ANIM_bonecoll_new()`;
- when ensuring that a new bone collection has a unique name;
- when renaming a bone collection;
- in the bone collection tests, to check that new bones have the
  expected translated name.

It also sets the default value of the bone name in the
`ARMATURE_OT_bone_primitive_add()` operator to a null string instead
of "Bone", so that the default name may be chosen while checking for
unique names, since an empty string will default to the translation.

Pull Request: https://projects.blender.org/blender/blender/pulls/113171
2023-10-05 14:38:51 +02:00

576 lines
18 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup animrig
*/
#include "BLI_linklist.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_color.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.h"
#include "BLI_utildefines.h"
#include "BLT_translation.h"
#include "DNA_armature_types.h"
#include "BLI_math_bits.h"
#include "MEM_guardedalloc.h"
#include "BKE_animsys.h"
#include "BKE_idprop.h"
#include "BKE_lib_id.h"
#include "ANIM_armature_iter.hh"
#include "ANIM_bone_collections.hh"
#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;
constexpr auto bonecoll_default_name = "Bones";
} // namespace
BoneCollection *ANIM_bonecoll_new(const char *name)
{
if (name == nullptr || name[0] == '\0') {
/* Use a default name if no name was given. */
name = DATA_(bonecoll_default_name);
}
/* Note: the collection name may change after the collection is added to an
* armature, to ensure it is unique within the armature. */
BoneCollection *bcoll = MEM_cnew<BoneCollection>(__func__);
STRNCPY_UTF8(bcoll->name, 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");
if (bcoll->prop) {
IDP_FreeProperty(bcoll->prop);
}
MEM_delete(bcoll);
}
/**
* Construct the mapping from the bones to this collection.
*
* This assumes that the bones do not have such a pointer yet, i.e. calling this
* twice for the same bone collection will cause duplicate pointers. */
static void add_reverse_pointers(BoneCollection *bcoll)
{
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_refresh(bArmature *armature)
{
ANIM_armature_runtime_free(armature);
BoneCollection *active = ANIM_armature_bonecoll_get_by_name(armature,
armature->active_collection_name);
ANIM_armature_bonecoll_active_set(armature, active);
/* Construct the bone-to-collections mapping. */
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
add_reverse_pointers(bcoll);
}
}
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,
DATA_(bonecoll_default_name),
'.',
offsetof(BoneCollection, name),
sizeof(bcoll->name));
}
BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name)
{
BoneCollection *bcoll = ANIM_bonecoll_new(name);
if (!ID_IS_LINKED(&armature->id) && ID_IS_OVERRIDE_LIBRARY(&armature->id)) {
/* Mark this bone collection as local override, so that certain operations can be allowed. */
bcoll->flags |= BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL;
}
bonecoll_ensure_name_unique(armature, bcoll);
BLI_addtail(&armature->collections, bcoll);
return bcoll;
}
BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature,
BoneCollection *anchor,
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);
}
if (bcoll_to_copy->prop) {
bcoll->prop = IDP_CopyProperty_ex(bcoll_to_copy->prop,
0 /*do_id_user ? 0 : LIB_ID_CREATE_NO_USER_REFCOUNT*/);
}
BLI_insertlinkafter(&armature->collections, anchor, bcoll);
bonecoll_ensure_name_unique(armature, bcoll);
add_reverse_pointers(bcoll);
return bcoll;
}
static void armature_bonecoll_active_clear(bArmature *armature)
{
armature->runtime.active_collection_index = -1;
armature->runtime.active_collection = nullptr;
armature->active_collection_name[0] = '\0';
}
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;
}
STRNCPY(armature->active_collection_name, bcoll->name);
armature->runtime.active_collection_index = index;
armature->runtime.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;
}
STRNCPY(armature->active_collection_name, bcoll->name);
armature->runtime.active_collection_index = bone_collection_index;
armature->runtime.active_collection = bcoll;
}
void ANIM_armature_bonecoll_active_name_set(bArmature *armature, const char *name)
{
BoneCollection *bcoll = ANIM_armature_bonecoll_get_by_name(armature, name);
ANIM_armature_bonecoll_active_set(armature, bcoll);
}
bool ANIM_armature_bonecoll_is_editable(const bArmature *armature, const BoneCollection *bcoll)
{
const bool is_override = ID_IS_OVERRIDE_LIBRARY(armature);
if (ID_IS_LINKED(armature) && !is_override) {
return false;
}
if (is_override && (bcoll->flags & BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL) == 0) {
return false;
}
return true;
}
bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, const int step)
{
if (bcoll == nullptr) {
return false;
}
if (!BLI_listbase_link_move(&armature->collections, bcoll, step)) {
return false;
}
if (bcoll == armature->runtime.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)];
STRNCPY(old_name, bcoll->name);
if (name[0] == '\0') {
/* Refuse to have nameless collections. The name of the active collection is stored in DNA, and
* an empty string means 'no active collection'. */
STRNCPY(bcoll->name, DATA_(bonecoll_default_name));
}
else {
STRNCPY_UTF8(bcoll->name, 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_show(BoneCollection *bcoll)
{
bcoll->flags |= BONE_COLLECTION_VISIBLE;
}
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);
}
/* Store reverse membership on the bone. */
static void add_reference(Bone *bone, BoneCollection *bcoll)
{
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&bone->runtime.collections, ref);
}
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);
add_reference(bone, bcoll);
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)
{
ANIM_armature_bonecoll_unassign_all(bone);
return ANIM_armature_bonecoll_assign(bcoll, bone);
}
bool ANIM_armature_bonecoll_assign_and_move_editbone(BoneCollection *bcoll, EditBone *ebone)
{
ANIM_armature_bonecoll_unassign_all_editbone(ebone);
return ANIM_armature_bonecoll_assign_editbone(bcoll, ebone);
}
bool ANIM_armature_bonecoll_unassign(BoneCollection *bcoll, Bone *bone)
{
bool was_found = false;
/* Remove membership 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;
}
void ANIM_armature_bonecoll_unassign_all(Bone *bone)
{
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &bone->runtime.collections) {
/* TODO: include Armature as parameter, and check that the bone collection to unassign from is
* actually editable. */
ANIM_armature_bonecoll_unassign(ref->bcoll, bone);
}
}
void ANIM_armature_bonecoll_unassign_all_editbone(EditBone *ebone)
{
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &ebone->bone_collections) {
ANIM_armature_bonecoll_unassign_editbone(ref->bcoll, ebone);
}
}
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(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 bArmature * /*armature*/, const Bone *bone)
{
return any_bone_collection_visible(&bone->runtime.collections);
}
bool ANIM_bonecoll_is_visible_editbone(const bArmature * /*armature*/, const EditBone *ebone)
{
return any_bone_collection_visible(&ebone->bone_collections);
}
void ANIM_armature_bonecoll_show_all(bArmature *armature)
{
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
ANIM_bonecoll_show(bcoll);
}
}
void ANIM_armature_bonecoll_hide_all(bArmature *armature)
{
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
ANIM_bonecoll_hide(bcoll);
}
}
/* ********************************* */
/* Armature Layers transitional API. */
void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *ebone)
{
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);
}
void ANIM_armature_bonecoll_show_from_bone(bArmature *armature, const Bone *bone)
{
if (ANIM_bonecoll_is_visible(armature, bone)) {
return;
}
/* 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_bonecoll_show_from_ebone(bArmature *armature, const EditBone *ebone)
{
if (ANIM_bonecoll_is_visible_editbone(armature, ebone)) {
return;
}
/* 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_bonecoll_show_from_pchan(bArmature *armature, const bPoseChannel *pchan)
{
ANIM_armature_bonecoll_show_from_bone(armature, pchan->bone);
}
/* ********* */
/* C++ only. */
namespace blender::animrig {
/* Utility functions for Armature edit-mode undo. */
blender::Map<BoneCollection *, BoneCollection *> ANIM_bonecoll_listbase_copy_no_membership(
ListBase *bone_colls_dst, ListBase *bone_colls_src, const bool do_id_user)
{
BLI_assert(BLI_listbase_is_empty(bone_colls_dst));
blender::Map<BoneCollection *, BoneCollection *> bcoll_map{};
LISTBASE_FOREACH (BoneCollection *, bcoll_src, bone_colls_src) {
BoneCollection *bcoll_dst = static_cast<BoneCollection *>(MEM_dupallocN(bcoll_src));
/* This will be rebuilt from the edit bones, so we don't need to copy it. */
BLI_listbase_clear(&bcoll_dst->bones);
if (bcoll_src->prop) {
bcoll_dst->prop = IDP_CopyProperty_ex(bcoll_src->prop,
do_id_user ? 0 : LIB_ID_CREATE_NO_USER_REFCOUNT);
}
BLI_addtail(bone_colls_dst, bcoll_dst);
bcoll_map.add(bcoll_src, bcoll_dst);
}
return bcoll_map;
}
void ANIM_bonecoll_listbase_free(ListBase *bcolls, const bool do_id_user)
{
LISTBASE_FOREACH_MUTABLE (BoneCollection *, bcoll, bcolls) {
if (bcoll->prop) {
IDP_FreeProperty_ex(bcoll->prop, do_id_user);
}
/* This will usually already be empty, because the passed BoneCollection
* list is usually from ANIM_bonecoll_listbase_copy_no_membership().
* However, during undo this is also used to free the BoneCollection
* list on the Armature itself before copying over the undo BoneCollection
* list, in which case this of Bone pointers may not be empty. */
BLI_freelistN(&bcoll->bones);
}
BLI_freelistN(bcolls);
}
} // namespace blender::animrig