From 623ff64a278924af57d7e1ec7e7bdb8792a560f8 Mon Sep 17 00:00:00 2001 From: Sebastian Parborg Date: Fri, 4 Feb 2022 14:19:44 +0100 Subject: Fix T81541: Symmetrize Transform Constraint, Y rotational axis unexpected results The case where Y rotation is mapped to Y rotation was not handled. This is now fixed. Also added an automated test to make sure that the symmetrize operator functions as intended. Reviewed By: Sybren Differential Revision: http://developer.blender.org/D9214 --- tests/python/CMakeLists.txt | 7 + tests/python/bl_rigging_symmetrize.py | 244 ++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 tests/python/bl_rigging_symmetrize.py (limited to 'tests') diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 63dcdc0f925..04fdb380da2 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -293,6 +293,13 @@ add_blender_test( --testdir "${TEST_SRC_DIR}/animation" ) +add_blender_test( + bl_rigging_symmetrize + --python ${CMAKE_CURRENT_LIST_DIR}/bl_rigging_symmetrize.py + -- + --testdir "${TEST_SRC_DIR}/animation" +) + # ------------------------------------------------------------------------------ # IO TESTS diff --git a/tests/python/bl_rigging_symmetrize.py b/tests/python/bl_rigging_symmetrize.py new file mode 100644 index 00000000000..b47ace7f3f1 --- /dev/null +++ b/tests/python/bl_rigging_symmetrize.py @@ -0,0 +1,244 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +""" +blender -b -noaudio --factory-startup --python tests/python/bl_rigging_symmetrize.py -- --testdir /path/to/lib/tests/animation +""" + +import pathlib +import sys +import unittest + +import bpy + + +def check_loc_rot_scale(self, bone, exp_bone): + # Check if posistions are the same + self.assertEqualVector( + bone.head, exp_bone.head, "Head position", bone.name) + self.assertEqualVector( + bone.tail, exp_bone.tail, "Tail position", bone.name) + + # Scale + self.assertEqualVector( + bone.scale, exp_bone.scale, "Scale", bone.name) + + # Rotation + rot_mode = exp_bone.rotation_mode + self.assertEqual(bone.rotation_mode, rot_mode, "Rotations mode does not match on bone %s" % (bone.name)) + + if rot_mode == 'QUATERNION': + self.assertEqualVector( + bone.rotation_quaternion, exp_bone.rotation_quaternion, "Quaternion rotation", bone.name) + elif rot_mode == 'AXIS_ANGLE': + self.assertEqualVector( + bone.axis_angle, exp_bone.axis_angle, "Axis Angle rotation", bone.name) + else: + # Euler rotation + self.assertEqualVector( + bone.rotation_euler, exp_bone.rotation_euler, "Euler rotation", bone.name) + + +def check_parent(self, bone, exp_bone): + self.assertEqual(type(bone.parent), type(exp_bone.parent), + "Missmatching types in pose.bones[%s].parent" % (bone.name)) + self.assertTrue(bone.parent is None or bone.parent.name == exp_bone.parent.name, + "Bone parent does not match on bone %s" % (bone.name)) + + +def check_bendy_bones(self, bone, exp_bone): + bone_variables = bone.bl_rna.properties.keys() + + bendy_bone_variables = [ + var for var in bone_variables if var.startswith("bbone_")] + + for var in bendy_bone_variables: + value = getattr(bone, var) + exp_value = getattr(exp_bone, var) + + self.assertEqual(type(value), type(exp_value), + "Missmatching types in pose.bones[%s].%s" % (bone.name, var)) + + if isinstance(value, str): + self.assertEqual(value, exp_value, + "Missmatching value in pose.bones[%s].%s" % (bone.name, var)) + elif hasattr(value, "name"): + self.assertEqual(value.name, exp_value.name, + "Missmatching value in pose.bones[%s].%s" % (bone.name, var)) + else: + self.assertAlmostEqual(value, exp_value, + "Missmatching value in pose.bones[%s].%s" % (bone.name, var)) + + +def check_ik(self, bone, exp_bone): + bone_variables = bone.bl_rna.properties.keys() + prefixes = ("ik_", "lock_ik", "use_ik") + ik_bone_variables = ( + var for var in bone_variables + if var.startswith(prefixes) + ) + + for var in ik_bone_variables: + value = getattr(bone, var) + exp_value = getattr(exp_bone, var) + self.assertAlmostEqual(value, exp_value, + "Missmatching value in pose.bones[%s].%s" % (bone.name, var)) + + +def check_constraints(self, input_arm, expected_arm, bone, exp_bone): + const_len = len(bone.constraints) + expo_const_len = len(exp_bone.constraints) + + self.assertEqual(const_len, expo_const_len, + "Constraints missmatch on bone %s" % (bone.name)) + + for exp_constraint in exp_bone.constraints: + const_name = exp_constraint.name + # Make sure that the constraint exists + self.assertTrue(const_name in bone.constraints, + "Bone %s is expected to contain constraint %s, but it does not." % ( + bone.name, const_name)) + constraint = bone.constraints[const_name] + const_variables = constraint.bl_rna.properties.keys() + + for var in const_variables: + + if var == "is_override_data": + # This variable is not used for local (non linked) data. + # For local object it is not initialized, so don't check this value. + continue + + value = getattr(constraint, var) + exp_value = getattr(exp_constraint, var) + + self.assertEqual(type(value), type(exp_value), + "Missmatching constraint value types in pose.bones[%s].constraints[%s].%s" % ( + bone.name, const_name, var)) + + if isinstance(value, str): + self.assertEqual(value, exp_value, + "Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % ( + bone.name, const_name, var)) + elif hasattr(value, "name"): + # Some constraints targets the armature itself, so the armature name should missmatch. + if value.name == input_arm.name and exp_value.name == expected_arm.name: + continue + + self.assertEqual(value.name, exp_value.name, + "Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % ( + bone.name, const_name, var)) + + elif isinstance(value, bool): + self.assertEqual(value, exp_value, + "Missmatching constraint boolean in pose.bones[%s].constraints[%s].%s" % ( + bone.name, const_name, var)) + else: + self.assertAlmostEqual(value, exp_value, + "Missmatching constraint value in pose.bones[%s].constraints[%s].%s" % ( + bone.name, const_name, var)) + + +class AbstractAnimationTest: + @classmethod + def setUpClass(cls): + cls.testdir = args.testdir + + def setUp(self): + self.assertTrue(self.testdir.exists(), + 'Test dir %s should exist' % self.testdir) + + +class ArmatureSymmetrizeTest(AbstractAnimationTest, unittest.TestCase): + def test_symmetrize_operator(self): + """Test that the symmetrize operator is working correctly.""" + bpy.ops.wm.open_mainfile(filepath=str( + self.testdir / "symm_test.blend")) + + # T81541 (D9214) + arm = bpy.data.objects['transform_const_rig'] + expected_arm = bpy.data.objects['expected_transform_const_rig'] + self.assertEqualSymmetrize(arm, expected_arm) + + # T66751 (D6009) + arm = bpy.data.objects['dragon_rig'] + expected_arm = bpy.data.objects['expected_dragon_rig'] + self.assertEqualSymmetrize(arm, expected_arm) + + def assertEqualSymmetrize(self, input_arm, expected_arm): + + # Symmetrize our input armature + bpy.context.view_layer.objects.active = input_arm + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.armature.select_all(action='SELECT') + bpy.ops.armature.symmetrize() + bpy.ops.object.mode_set(mode='OBJECT') + + # Make sure that the bone count is the same + bone_len = len(input_arm.pose.bones) + expected_bone_len = len(expected_arm.pose.bones) + self.assertEqual(bone_len, expected_bone_len, + "Expected bone count to match") + + for exp_bone in expected_arm.pose.bones: + bone_name = exp_bone.name + # Make sure that the bone exists + self.assertTrue(bone_name in input_arm.pose.bones, + "Armature is expected to contain bone %s, but it does not." % (bone_name)) + bone = input_arm.pose.bones[bone_name] + + # Loc Rot Scale + check_loc_rot_scale(self, bone, exp_bone) + + # Parent settings + check_parent(self, bone, exp_bone) + + # Bendy Bones + check_bendy_bones(self, bone, exp_bone) + + # IK + check_ik(self, bone, exp_bone) + + # Constraints + check_constraints(self, input_arm, expected_arm, bone, exp_bone) + + def assertEqualVector(self, vec1, vec2, check_str, bone_name) -> None: + for idx, value in enumerate(vec1): + self.assertAlmostEqual( + value, vec2[idx], 3, "%s does not match with expected value on bone %s" % (check_str, bone_name)) + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main() -- cgit v1.2.3