Anim: Symmetrize collection assignments when symmetrizing Armatures
This patch makes it so collection assignments of bones are symmetrized when the "Symmetrize" operator is run. * Only collections with names that can be symmetrized are modified. * Collections are created if they don't exist, they are created under the same parent as the source collection * In the case of a "left" bone assigned to a "right" collection the resulting "right" bone will be assigned to a "left" collection. The system does explicitly not try to be smart here. Also adds unit tests. Pull Request: https://projects.blender.org/blender/blender/pulls/130524
This commit is contained in:
committed by
Christoph Lendenfeld
parent
dfc0d97abf
commit
16c2e71ab0
@@ -996,6 +996,49 @@ static void mirror_pose_bone(Object &ob, EditBone &ebone)
|
||||
pose_bone->limitmax[2] = -limit_min;
|
||||
}
|
||||
|
||||
static void mirror_bone_collection_assignments(bArmature &armature,
|
||||
EditBone &source_bone,
|
||||
EditBone &target_bone)
|
||||
{
|
||||
BLI_assert_msg(armature.edbo != nullptr, "Expecting the armature to be in edit mode");
|
||||
char name_flip[64];
|
||||
/* Avoiding modification of the ListBase in the iteration. */
|
||||
blender::Vector<BoneCollection *> unassign_collections;
|
||||
blender::Vector<BoneCollection *> assign_collections;
|
||||
|
||||
/* Find all collections from source_bone that can be flipped. */
|
||||
LISTBASE_FOREACH (BoneCollectionReference *, collection_reference, &source_bone.bone_collections)
|
||||
{
|
||||
BoneCollection *collection = collection_reference->bcoll;
|
||||
BLI_string_flip_side_name(name_flip, collection->name, false, sizeof(name_flip));
|
||||
if (STREQ(name_flip, collection->name)) {
|
||||
/* Name flipping failed. */
|
||||
continue;
|
||||
}
|
||||
BoneCollection *flipped_collection = ANIM_armature_bonecoll_get_by_name(&armature, name_flip);
|
||||
if (!flipped_collection) {
|
||||
const int bcoll_index = blender::animrig::armature_bonecoll_find_index(&armature,
|
||||
collection);
|
||||
const int parent_index = blender::animrig::armature_bonecoll_find_parent_index(&armature,
|
||||
bcoll_index);
|
||||
flipped_collection = ANIM_armature_bonecoll_new(&armature, name_flip, parent_index);
|
||||
}
|
||||
BLI_assert(flipped_collection != nullptr);
|
||||
unassign_collections.append(collection);
|
||||
assign_collections.append(flipped_collection);
|
||||
}
|
||||
|
||||
/* The target_bone might not be in unassign_collections anymore, or might already be in
|
||||
* assign_collections. The assign functions will just do nothing in those cases. */
|
||||
for (BoneCollection *collection : unassign_collections) {
|
||||
ANIM_armature_bonecoll_unassign_editbone(collection, &target_bone);
|
||||
}
|
||||
|
||||
for (BoneCollection *collection : assign_collections) {
|
||||
ANIM_armature_bonecoll_assign_editbone(collection, &target_bone);
|
||||
}
|
||||
}
|
||||
|
||||
static void copy_pchan(EditBone *src_bone, EditBone *dst_bone, Object *src_ob, Object *dst_ob)
|
||||
{
|
||||
/* copy the ID property */
|
||||
@@ -1422,6 +1465,7 @@ static int armature_symmetrize_exec(bContext *C, wmOperator *op)
|
||||
update_duplicate_custom_bone_shapes(C, ebone, obedit);
|
||||
/* Mirror any settings on the pose bone. */
|
||||
mirror_pose_bone(*obedit, *ebone);
|
||||
mirror_bone_collection_assignments(*arm, *ebone_iter, *ebone);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -324,6 +324,92 @@ class ArmatureSymmetrizeTargetsTest(unittest.TestCase):
|
||||
self.assertEqual(symm_constraint.subtarget, "invalid_subtarget")
|
||||
|
||||
|
||||
class ArmatureSymmetrizeCollectionAssignments(unittest.TestCase):
|
||||
arm_ob: bpy.types.Object
|
||||
arm: bpy.types.Armature
|
||||
|
||||
def setUp(self):
|
||||
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
||||
self.arm_ob, self.arm = create_armature()
|
||||
bpy.context.view_layer.objects.active = self.arm_ob
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
ebone = self.arm.edit_bones.new(name="test.l")
|
||||
ebone.tail = (1, 0, 0)
|
||||
|
||||
parent_coll = self.arm.collections.new("parent")
|
||||
left_coll = self.arm.collections.new("collection.l", parent=parent_coll)
|
||||
self.assertTrue(left_coll.assign(ebone))
|
||||
self.assertEqual(len(ebone.collections), 1)
|
||||
|
||||
def test_symmetrize_to_existing_collection(self):
|
||||
other_parent = self.arm.collections.new("other_parent")
|
||||
right_coll = self.arm.collections.new("collection.r", parent=other_parent)
|
||||
|
||||
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
|
||||
bpy.ops.armature.symmetrize()
|
||||
|
||||
right_bone = self.arm.edit_bones["test.r"]
|
||||
self.assertEqual(len(right_bone.collections), 1)
|
||||
self.assertEqual(right_bone.collections[0], right_coll)
|
||||
|
||||
# Parents should not be modified.
|
||||
left_coll = self.arm.collections_all["collection.l"]
|
||||
self.assertNotEqual(right_coll.parent, left_coll.parent)
|
||||
|
||||
def test_no_symmetrize(self):
|
||||
# If the collection name cannot be flipped, nothing changes.
|
||||
non_flip_collection = self.arm.collections.new("foobar")
|
||||
left_bone = self.arm.edit_bones["test.l"]
|
||||
self.arm.collections_all["collection.l"].unassign(left_bone)
|
||||
self.assertTrue(non_flip_collection.assign(left_bone))
|
||||
|
||||
set_edit_bone_selected(left_bone, True)
|
||||
bpy.ops.armature.symmetrize()
|
||||
|
||||
right_bone = self.arm.edit_bones["test.r"]
|
||||
self.assertEqual(len(right_bone.collections), 1)
|
||||
self.assertEqual(right_bone.collections[0], non_flip_collection)
|
||||
|
||||
def test_create_missing_collection(self):
|
||||
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
|
||||
self.assertFalse("collection.r" in self.arm.collections_all)
|
||||
bpy.ops.armature.symmetrize()
|
||||
|
||||
# Missing collections are created.
|
||||
self.assertTrue("collection.r" in self.arm.collections_all)
|
||||
right_coll = self.arm.collections_all["collection.r"]
|
||||
# When the collection is created, it is parented to the same collection as the source collection.
|
||||
left_coll = self.arm.collections_all["collection.l"]
|
||||
self.assertEqual(right_coll.parent, left_coll.parent)
|
||||
|
||||
right_bone = self.arm.edit_bones["test.r"]
|
||||
self.assertEqual(len(right_bone.collections), 1)
|
||||
self.assertEqual(right_bone.collections[0], right_coll)
|
||||
|
||||
def test_symmetrize_to_existing_bone(self):
|
||||
right_bone = self.arm.edit_bones.new(name="test.r")
|
||||
right_bone.tail = (1, 0, 0)
|
||||
unique_right_coll = self.arm.collections.new("unique")
|
||||
unique_right_coll.assign(right_bone)
|
||||
|
||||
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
|
||||
bpy.ops.armature.symmetrize()
|
||||
|
||||
# Missing collection is created.
|
||||
self.assertTrue("collection.r" in self.arm.collections_all)
|
||||
self.assertEqual(len(right_bone.collections), 2)
|
||||
self.assertTrue("collection.r" in right_bone.collections)
|
||||
self.assertTrue("unique" in right_bone.collections,
|
||||
"Mirrored bone shouldn't have lost the unique collection assignment")
|
||||
|
||||
# Symmetrizing twice shouldn't double invert the collection assignments.
|
||||
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
|
||||
set_edit_bone_selected(right_bone, False)
|
||||
bpy.ops.armature.symmetrize()
|
||||
self.assertTrue("collection.r" in right_bone.collections)
|
||||
self.assertTrue("collection.l" not in right_bone.collections)
|
||||
|
||||
|
||||
def main():
|
||||
global args
|
||||
import argparse
|
||||
|
||||
Reference in New Issue
Block a user