Changeset View
Changeset View
Standalone View
Standalone View
tests/python/bl_rigging_symmetrize.py
- This file was added.
| # ##### 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 ##### | |||||
| # <pep8 compliant> | |||||
| """ | |||||
| 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): | |||||
sybren: This function is far too complex, which is evident from its nesting depth. It should be split… | |||||
| 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), | |||||
Done Inline ActionsWhy would a None value not be compared to the expected value? If the expected value is not None, this would silently ignore an error case. sybren: Why would a `None` value not be compared to the expected value? If the expected value is not… | |||||
| "Missmatching constraint value types in pose.bones[%s].constraints[%s].%s" % ( | |||||
Done Inline ActionsDon't use type(value) == sometype, use isinstance(value, sometype) instead. sybren: Don't use `type(value) == sometype`, use `isinstance(value, sometype)` instead. | |||||
| 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): | |||||
Done Inline ActionsThere is no need to iterate three times over bone_variables, and it looks like you don't even need to construct a list. Just define a set of prefixes, then use a double generator expression, like: prefixes = ("ik_", "lock_ik", "use_ik")
ik_bone_variables = (
var for var in bone_variables
for prefix in prefixes
if var.startswith(prefix)
)sybren: There is no need to iterate three times over `bone_variables`, and it looks like you don't even… | |||||
Done Inline ActionsMy Python is getting rusty. str.startswith() supports tuples for prefixes, so you can use: prefixes = ("ik_", "lock_ik", "use_ik")
ik_bone_variables = (
var for var in bone_variables
if var.startswith(prefixes)
)sybren: My Python is getting rusty. [str.startswith()](https://docs.python.org/3/library/stdtypes. | |||||
| 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() | |||||
This function is far too complex, which is evident from its nesting depth. It should be split up into smaller functions.