e955c94ed3
Listing the "Blender Foundation" as copyright holder implied the Blender Foundation holds copyright to files which may include work from many developers. While keeping copyright on headers makes sense for isolated libraries, Blender's own code may be refactored or moved between files in a way that makes the per file copyright holders less meaningful. Copyright references to the "Blender Foundation" have been replaced with "Blender Authors", with the exception of `./extern/` since these this contains libraries which are more isolated, any changed to license headers there can be handled on a case-by-case basis. Some directories in `./intern/` have also been excluded: - `./intern/cycles/` it's own `AUTHORS` file is planned. - `./intern/opensubdiv/`. An "AUTHORS" file has been added, using the chromium projects authors file as a template. Design task: #110784 Ref !110783.
229 lines
8.4 KiB
Python
229 lines
8.4 KiB
Python
# SPDX-FileCopyrightText: 2022-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
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),
|
|
"Mismatching 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),
|
|
"Mismatching types in pose.bones[%s].%s" % (bone.name, var))
|
|
|
|
if isinstance(value, str):
|
|
self.assertEqual(value, exp_value,
|
|
"Mismatching value in pose.bones[%s].%s" % (bone.name, var))
|
|
elif hasattr(value, "name"):
|
|
self.assertEqual(value.name, exp_value.name,
|
|
"Mismatching value in pose.bones[%s].%s" % (bone.name, var))
|
|
else:
|
|
self.assertAlmostEqual(value, exp_value,
|
|
"Mismatching 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,
|
|
"Mismatching 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 mismatch 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),
|
|
"Mismatching constraint value types in pose.bones[%s].constraints[%s].%s" % (
|
|
bone.name, const_name, var))
|
|
|
|
if isinstance(value, str):
|
|
self.assertEqual(value, exp_value,
|
|
"Mismatching 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 mismatch.
|
|
if value.name == input_arm.name and exp_value.name == expected_arm.name:
|
|
continue
|
|
|
|
self.assertEqual(value.name, exp_value.name,
|
|
"Mismatching constraint value in pose.bones[%s].constraints[%s].%s" % (
|
|
bone.name, const_name, var))
|
|
|
|
elif isinstance(value, bool):
|
|
self.assertEqual(value, exp_value,
|
|
"Mismatching constraint boolean in pose.bones[%s].constraints[%s].%s" % (
|
|
bone.name, const_name, var))
|
|
else:
|
|
msg = "Mismatching constraint value in pose.bones[%s].constraints[%s].%s" % (
|
|
bone.name, const_name, var)
|
|
self.assertAlmostEqual(value, exp_value, places=6, msg=msg)
|
|
|
|
|
|
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"))
|
|
|
|
# #81541 (D9214)
|
|
arm = bpy.data.objects['transform_const_rig']
|
|
expected_arm = bpy.data.objects['expected_transform_const_rig']
|
|
self.assertEqualSymmetrize(arm, expected_arm)
|
|
|
|
# #66751 (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()
|