Fix #129094: Sub-targets not symmetrized if from different armature

When symmetrizing a bone (in edit mode) any constraint subtargets were name flipped,
if possible and they exist. This only worked if the target is the armature of the bone
being flipped.
This patch changes that so a subtarget flip is always attempted for targets that are an armature.

Pull Request: https://projects.blender.org/blender/blender/pulls/129169
This commit is contained in:
Christoph Lendenfeld
2024-11-14 12:04:58 +01:00
committed by Christoph Lendenfeld
parent 36aca3c132
commit f025ff81fc
2 changed files with 124 additions and 21 deletions

View File

@@ -295,11 +295,13 @@ EditBone *add_points_bone(Object *obedit, float head[3], float tail[3])
static EditBone *get_named_editbone(ListBase *edbo, const char *name)
{
if (name) {
LISTBASE_FOREACH (EditBone *, eBone, edbo) {
if (STREQ(name, eBone->name)) {
return eBone;
}
if (!edbo || !name) {
return nullptr;
}
LISTBASE_FOREACH (EditBone *, eBone, edbo) {
if (STREQ(name, eBone->name)) {
return eBone;
}
}
@@ -387,7 +389,6 @@ static void pose_edit_bone_duplicate(ListBase *editbones, Object *ob)
}
static void update_duplicate_subtarget(EditBone *dup_bone,
ListBase *editbones,
Object *ob,
const bool lookup_mirror_subtarget)
{
@@ -402,6 +403,7 @@ static void update_duplicate_subtarget(EditBone *dup_bone,
EditBone *oldtarget, *newtarget;
ListBase *conlist = &pchan->constraints;
char name_flipped[MAX_ID_NAME - 2];
LISTBASE_FOREACH (bConstraint *, curcon, conlist) {
/* does this constraint have a subtarget in
* this armature?
@@ -412,30 +414,28 @@ static void update_duplicate_subtarget(EditBone *dup_bone,
continue;
}
LISTBASE_FOREACH (bConstraintTarget *, ct, &targets) {
if ((ct->tar != ob) && (!ct->subtarget[0])) {
if (!ct->tar || !ct->subtarget[0]) {
continue;
}
oldtarget = get_named_editbone(editbones, ct->subtarget);
if (!oldtarget) {
Object *target_ob = ct->tar;
if (target_ob->type != OB_ARMATURE || !target_ob->data) {
/* Can only mirror armature. */
continue;
}
/* was the subtarget bone duplicated too? If
bArmature *target_armature = static_cast<bArmature *>(target_ob->data);
/* Was the subtarget bone duplicated too? If
* so, update the constraint to point at the
* duplicate of the old subtarget.
*/
if (oldtarget->temp.ebone) {
oldtarget = get_named_editbone(&target_armature->bonebase, ct->subtarget);
if (oldtarget && oldtarget->temp.ebone) {
newtarget = oldtarget->temp.ebone;
STRNCPY(ct->subtarget, newtarget->name);
}
else if (lookup_mirror_subtarget) {
/* The subtarget was not selected for duplication, try to see if a mirror bone of
* the current target exists */
char name_flip[MAXBONENAME];
BLI_string_flip_side_name(name_flip, oldtarget->name, false, sizeof(name_flip));
newtarget = get_named_editbone(editbones, name_flip);
if (newtarget) {
STRNCPY(ct->subtarget, newtarget->name);
BLI_string_flip_side_name(name_flipped, ct->subtarget, false, sizeof(name_flipped));
if (bPoseChannel *flipped_bone = BKE_pose_channel_find_name(ct->tar->pose, name_flipped)) {
STRNCPY(ct->subtarget, flipped_bone->name);
}
}
}
@@ -1161,7 +1161,7 @@ static int armature_duplicate_selected_exec(bContext *C, wmOperator *op)
}
/* Lets try to fix any constraint sub-targets that might have been duplicated. */
update_duplicate_subtarget(ebone, arm->edbo, ob, false);
update_duplicate_subtarget(ebone, ob, false);
}
}
@@ -1414,7 +1414,7 @@ static int armature_symmetrize_exec(bContext *C, wmOperator *op)
ebone->bbone_next_flag = ebone_iter->bbone_next_flag;
/* Lets try to fix any constraint sub-targets that might have been duplicated. */
update_duplicate_subtarget(ebone, arm->edbo, obedit, true);
update_duplicate_subtarget(ebone, obedit, true);
/* Try to update constraint options so that they are mirrored as well
* (need to supply bone_iter as well in case we are working with existing bones) */
update_duplicate_constraint_settings(ebone, ebone_iter, obedit);

View File

@@ -221,6 +221,109 @@ class ArmatureSymmetrizeTest(AbstractAnimationTest, unittest.TestCase):
value, vec2[idx], 3, "%s does not match with expected value on bone %s" % (check_str, bone_name))
def create_armature() -> tuple[bpy.types.Object, bpy.types.Armature]:
arm = bpy.data.armatures.new('Armature')
arm_ob = bpy.data.objects.new('ArmObject', arm)
# Link to the scene just for giggles. And ease of debugging when things
# go bad.
bpy.context.scene.collection.objects.link(arm_ob)
return arm_ob, arm
def set_edit_bone_selected(ebone: bpy.types.EditBone, selected: bool):
# Helper to select all parts of an edit bone.
ebone.select = selected
ebone.select_tail = selected
ebone.select_head = selected
def create_copy_loc_constraint(pose_bone, target_ob, subtarget):
pose_bone.constraints.new("COPY_LOCATION")
pose_bone.constraints[0].target = target_ob
pose_bone.constraints[0].subtarget = subtarget
class ArmatureSymmetrizeTargetsTest(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)
def test_symmetrize_selection(self):
# Only selected things are symmetrized.
set_edit_bone_selected(self.arm.edit_bones["test.l"], False)
bpy.ops.armature.symmetrize()
self.assertEqual(len(self.arm.edit_bones), 1, "If nothing is selected, no bone is symmetrized")
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
bpy.ops.armature.symmetrize()
self.assertEqual(len(self.arm.edit_bones), 2, "Selected EditBone should have been symmetrized")
self.assertTrue("test.r" in self.arm.edit_bones)
self.assertTrue("test.l" in self.arm.edit_bones)
def test_symmetrize_constraint_sub_target(self):
# Explicitly test that constraints targeting another armature are symmetrized.
bpy.ops.object.mode_set(mode='OBJECT')
target_arm_ob, target_arm = create_armature()
bpy.context.view_layer.objects.active = target_arm_ob
bpy.ops.object.mode_set(mode='EDIT')
target_arm.edit_bones.new("target.l")
target_arm.edit_bones.new("target.r")
target_arm.edit_bones["target.l"].tail = (1, 0, 0)
target_arm.edit_bones["target.r"].tail = (1, 0, 0)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = self.arm_ob
bpy.ops.object.mode_set(mode='POSE')
pose_bone_l = self.arm_ob.pose.bones["test.l"]
create_copy_loc_constraint(pose_bone_l, target_arm_ob, "target.l")
bpy.ops.object.mode_set(mode='EDIT')
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
bpy.ops.armature.symmetrize()
self.assertEqual(len(self.arm.edit_bones), 2, "Bone should have been symmetrized")
self.assertTrue("test.r" in self.arm.edit_bones)
self.assertTrue("test.l" in self.arm.edit_bones)
bpy.ops.object.mode_set(mode='POSE')
self.assertEqual(len(self.arm_ob.pose.bones["test.r"].constraints), 1, "Constraint should have been copied")
symm_constraint = self.arm_ob.pose.bones["test.r"].constraints[0]
self.assertEqual(symm_constraint.subtarget, "target.r")
def test_symmetrize_invalid_subtarget(self):
# Blender shouldn't crash when there is an invalid subtarget specified.
bpy.ops.object.mode_set(mode='OBJECT')
target_ob = bpy.data.objects.new("target", None)
bpy.context.scene.collection.objects.link(target_ob)
bpy.context.view_layer.objects.active = self.arm_ob
bpy.ops.object.mode_set(mode='POSE')
pose_bone_l = self.arm_ob.pose.bones["test.l"]
create_copy_loc_constraint(pose_bone_l, target_ob, "invalid_subtarget")
bpy.ops.object.mode_set(mode='EDIT')
set_edit_bone_selected(self.arm.edit_bones["test.l"], True)
bpy.ops.armature.symmetrize()
self.assertEqual(len(self.arm.edit_bones), 2, "Bone should have been symmetrized")
self.assertTrue("test.r" in self.arm.edit_bones)
self.assertTrue("test.l" in self.arm.edit_bones)
bpy.ops.object.mode_set(mode='POSE')
self.assertEqual(len(self.arm_ob.pose.bones["test.r"].constraints), 1, "Constraint should have been copied")
symm_constraint = self.arm_ob.pose.bones["test.r"].constraints[0]
self.assertEqual(symm_constraint.subtarget, "invalid_subtarget")
def main():
global args
import argparse