Sybren A. Stüvel
026eba8523
When the Child Of constraint is owned by a bone, before the constraint is run the matrix is converted from world to pose space. However, setting the inverse should also take the armature object's transform into account.
322 lines
15 KiB
Python
322 lines
15 KiB
Python
# ##### 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.bin --background -noaudio --factory-startup --python tests/python/bl_constraints.py -- --testdir /path/to/lib/tests/constraints
|
|
"""
|
|
|
|
import pathlib
|
|
import sys
|
|
import unittest
|
|
|
|
import bpy
|
|
from mathutils import Matrix
|
|
|
|
|
|
class AbstractConstraintTests(unittest.TestCase):
|
|
"""Useful functionality for constraint tests."""
|
|
|
|
layer_collection = '' # When set, this layer collection will be enabled.
|
|
|
|
def setUp(self):
|
|
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "constraints.blend"))
|
|
|
|
# This allows developers to disable layer collections and declutter the
|
|
# 3D Viewport while working in constraints.blend, without influencing
|
|
# the actual unit tests themselves.
|
|
if self.layer_collection:
|
|
top_collection = bpy.context.view_layer.layer_collection
|
|
collection = top_collection.children[self.layer_collection]
|
|
collection.exclude = False
|
|
|
|
def assert_matrix(self, actual_matrix, expect_matrix, object_name: str, places=6, delta=None):
|
|
"""Asserts that the matrices almost equal."""
|
|
self.assertEqual(len(actual_matrix), 4, 'Expected a 4x4 matrix')
|
|
|
|
# TODO(Sybren): decompose the matrices and compare loc, rot, and scale separately.
|
|
# That'll probably improve readability & understandability of test failures.
|
|
for row, (act_row, exp_row) in enumerate(zip(actual_matrix, expect_matrix)):
|
|
for col, (actual, expect) in enumerate(zip(act_row, exp_row)):
|
|
self.assertAlmostEqual(
|
|
actual, expect, places=places, delta=delta,
|
|
msg=f'Matrix of object {object_name!r} failed: {actual} != {expect} at element [{row}][{col}]')
|
|
|
|
def _get_eval_object(self, object_name: str) -> bpy.types.Object:
|
|
"""Return the evaluated object."""
|
|
depsgraph = bpy.context.view_layer.depsgraph
|
|
depsgraph.update()
|
|
ob_orig = bpy.context.scene.objects[object_name]
|
|
ob_eval = ob_orig.evaluated_get(depsgraph)
|
|
return ob_eval
|
|
|
|
def matrix(self, object_name: str) -> Matrix:
|
|
"""Return the evaluated world matrix."""
|
|
ob_eval = self._get_eval_object(object_name)
|
|
return ob_eval.matrix_world
|
|
|
|
def bone_matrix(self, object_name: str, bone_name: str) -> Matrix:
|
|
"""Return the evaluated world matrix of the bone."""
|
|
ob_eval = self._get_eval_object(object_name)
|
|
bone = ob_eval.pose.bones[bone_name]
|
|
return ob_eval.matrix_world @ bone.matrix
|
|
|
|
def matrix_test(self, object_name: str, expect: Matrix):
|
|
"""Assert that the object's world matrix is as expected."""
|
|
actual = self.matrix(object_name)
|
|
self.assert_matrix(actual, expect, object_name)
|
|
|
|
def bone_matrix_test(self, object_name: str, bone_name: str, expect: Matrix):
|
|
"""Assert that the bone's world matrix is as expected."""
|
|
actual = self.bone_matrix(object_name, bone_name)
|
|
self.assert_matrix(actual, expect, object_name)
|
|
|
|
def constraint_context(self, constraint_name: str, owner_name: str='') -> dict:
|
|
"""Return a context suitable for calling object constraint operators.
|
|
|
|
Assumes the owner is called "{constraint_name}.owner" if owner_name=''.
|
|
"""
|
|
owner = bpy.context.scene.objects[owner_name or f'{constraint_name}.owner']
|
|
constraint = owner.constraints[constraint_name]
|
|
context = {
|
|
**bpy.context.copy(),
|
|
'object': owner,
|
|
'active_object': owner,
|
|
'constraint': constraint,
|
|
}
|
|
return context
|
|
|
|
def bone_constraint_context(self, constraint_name: str, owner_name: str='', bone_name: str='') -> dict:
|
|
"""Return a context suitable for calling bone constraint operators.
|
|
|
|
Assumes the owner's object is called "{constraint_name}.owner" if owner_name=''.
|
|
Assumes the bone is called "{constraint_name}.bone" if bone_name=''.
|
|
"""
|
|
|
|
owner_name = owner_name or f'{constraint_name}.owner'
|
|
bone_name = bone_name or f'{constraint_name}.bone'
|
|
|
|
owner = bpy.context.scene.objects[owner_name]
|
|
pose_bone = owner.pose.bones[bone_name]
|
|
|
|
constraint = pose_bone.constraints[constraint_name]
|
|
context = {
|
|
**bpy.context.copy(),
|
|
'object': owner,
|
|
'active_object': owner,
|
|
'active_pose_bone': pose_bone,
|
|
'constraint': constraint,
|
|
'owner': pose_bone,
|
|
}
|
|
return context
|
|
|
|
|
|
class ChildOfTest(AbstractConstraintTests):
|
|
layer_collection = 'Child Of'
|
|
|
|
def test_object_simple_parent(self):
|
|
"""Child Of: simple evaluation of object parent."""
|
|
initial_matrix = Matrix((
|
|
(0.5872668623924255, -0.3642929494380951, 0.29567837715148926, 1.0886117219924927),
|
|
(0.31689348816871643, 0.7095895409584045, 0.05480116978287697, 2.178966999053955),
|
|
(-0.21244174242019653, 0.06738340109586716, 0.8475662469863892, 3.2520291805267334),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Child Of', owner_name='Child Of.object.owner')
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
|
(0.9992385506629944, 0.019844001159071922, -0.03359175845980644, 0.10000011324882507),
|
|
(-0.01744179055094719, 0.997369647026062, 0.07035345584154129, 0.1999998837709427),
|
|
(0.034899525344371796, -0.06971397250890732, 0.9969563484191895, 0.3000001311302185),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
def test_object_rotation_only(self):
|
|
"""Child Of: rotation only."""
|
|
owner = bpy.context.scene.objects['Child Of.object.owner']
|
|
constraint = owner.constraints['Child Of']
|
|
constraint.use_location_x = constraint.use_location_y = constraint.use_location_z = False
|
|
constraint.use_scale_x = constraint.use_scale_y = constraint.use_scale_z = False
|
|
|
|
initial_matrix = Matrix((
|
|
(0.8340795636177063, -0.4500490725040436, 0.31900957226753235, 0.10000000149011612),
|
|
(0.4547243118286133, 0.8883093595504761, 0.06428192555904388, 0.20000000298023224),
|
|
(-0.31230923533439636, 0.09144517779350281, 0.9455690383911133, 0.30000001192092896),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Child Of', owner_name='Child Of.object.owner')
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
|
(0.9992386102676392, 0.019843975082039833, -0.033591702580451965, 0.10000000149011612),
|
|
(-0.017441781237721443, 0.9973695874214172, 0.0703534483909607, 0.20000000298023224),
|
|
(0.03489946573972702, -0.06971397250890732, 0.9969563484191895, 0.30000001192092896),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
def test_object_no_x_axis(self):
|
|
"""Child Of: loc/rot/scale on only Y and Z axes."""
|
|
owner = bpy.context.scene.objects['Child Of.object.owner']
|
|
constraint = owner.constraints['Child Of']
|
|
constraint.use_location_x = False
|
|
constraint.use_rotation_x = False
|
|
constraint.use_scale_x = False
|
|
|
|
initial_matrix = Matrix((
|
|
(0.8294582366943359, -0.4013831615447998, 0.2102886438369751, 0.10000000149011612),
|
|
(0.46277597546577454, 0.6895919442176819, 0.18639995157718658, 2.2317214012145996),
|
|
(-0.31224438548088074, -0.06574578583240509, 0.8546382784843445, 3.219514846801758),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Child Of', owner_name='Child Of.object.owner',)
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
|
(0.9992386102676392, 0.019843991845846176, -0.03359176218509674, 0.10000000149011612),
|
|
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.2000001221895218),
|
|
(0.034899499267339706, -0.06971398741006851, 0.996956467628479, 0.3000001311302185),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
|
|
|
def test_bone_simple_parent(self):
|
|
"""Child Of: simple evaluation of bone parent."""
|
|
initial_matrix = Matrix((
|
|
(0.5133683681488037, -0.06418320536613464, -0.014910104684531689, -0.2277737855911255),
|
|
(-0.03355155512690544, 0.12542974948883057, -1.080492377281189, 2.082599639892578),
|
|
(0.019313642755150795, 1.0446348190307617, 0.1244703009724617, 2.8542778491973877),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Child Of.armature.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Child Of', owner_name='Child Of.armature.owner')
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
|
|
|
|
self.matrix_test('Child Of.armature.owner', Matrix((
|
|
(0.9992386102676392, 0.019843988120555878, -0.03359176218509674, 0.8358089923858643),
|
|
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 1.7178752422332764),
|
|
(0.03489949554204941, -0.06971397995948792, 0.9969563484191895, -1.8132872581481934),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.armature.owner', initial_matrix)
|
|
|
|
def test_bone_owner(self):
|
|
"""Child Of: bone owns constraint, targeting object."""
|
|
initial_matrix = Matrix((
|
|
(0.9992387890815735, -0.03359174728393555, -0.019843988120555878, -2.999999523162842),
|
|
(-0.02588011883199215, -0.1900751143693924, -0.9814283847808838, 2.0),
|
|
(0.029196053743362427, 0.9811949133872986, -0.190799742937088, 0.9999999403953552),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.bone_matrix_test('Child Of.bone.owner', 'Child Of.bone', initial_matrix)
|
|
|
|
context = self.bone_constraint_context('Child Of', owner_name='Child Of.bone.owner')
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of', owner='BONE')
|
|
|
|
self.bone_matrix_test('Child Of.bone.owner', 'Child Of.bone', Matrix((
|
|
(0.9659260511398315, 0.2588191032409668, 4.656613428188905e-10, -2.999999761581421),
|
|
(-3.725290742551124e-09, 1.4901162970204496e-08, -1.0, 0.9999999403953552),
|
|
(-0.2588191032409668, 0.965925931930542, 0.0, 0.9999999403953552),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of', owner='BONE')
|
|
self.bone_matrix_test('Child Of.bone.owner', 'Child Of.bone', initial_matrix)
|
|
|
|
def test_vertexgroup_simple_parent(self):
|
|
"""Child Of: simple evaluation of vertex group parent."""
|
|
initial_matrix = Matrix((
|
|
(-0.8076590895652771, 0.397272527217865, 0.4357309341430664, 1.188504934310913),
|
|
(-0.4534659683704376, -0.8908230066299438, -0.028334975242614746, 1.7851561307907104),
|
|
(0.3769024908542633, -0.22047416865825653, 0.8996308445930481, 3.4457669258117676),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Child Of.vertexgroup.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Child Of', owner_name='Child Of.vertexgroup.owner')
|
|
bpy.ops.constraint.childof_set_inverse(context, constraint='Child Of')
|
|
|
|
self.matrix_test('Child Of.vertexgroup.owner', Matrix((
|
|
(0.9992386102676392, 0.019843988120555878, -0.03359176218509674, 0.10000000149011612),
|
|
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.20000000298023224),
|
|
(0.03489949554204941, -0.06971397995948792, 0.9969563484191895, 0.30000001192092896),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(context, constraint='Child Of')
|
|
self.matrix_test('Child Of.vertexgroup.owner', initial_matrix)
|
|
|
|
|
|
class ObjectSolverTest(AbstractConstraintTests):
|
|
layer_collection = 'Object Solver'
|
|
|
|
def test_simple_parent(self):
|
|
"""Object Solver: simple evaluation of parent."""
|
|
initial_matrix = Matrix((
|
|
(-0.970368504524231, 0.03756079450249672, -0.23869234323501587, -0.681557297706604),
|
|
(-0.24130365252494812, -0.2019258439540863, 0.9492092132568359, 6.148940086364746),
|
|
(-0.012545102275907993, 0.9786801934242249, 0.20500603318214417, 2.2278831005096436),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
))
|
|
self.matrix_test('Object Solver.owner', initial_matrix)
|
|
|
|
context = self.constraint_context('Object Solver')
|
|
bpy.ops.constraint.objectsolver_set_inverse(context, constraint='Object Solver')
|
|
self.matrix_test('Object Solver.owner', Matrix((
|
|
(0.9992387294769287, 0.019843989983201027, -0.03359176591038704, 0.10000025480985641),
|
|
(-0.017441747710108757, 0.9973697662353516, 0.07035345584154129, 0.1999993920326233),
|
|
(0.034899502992630005, -0.06971398741006851, 0.996956467628479, 0.29999980330467224),
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
)))
|
|
|
|
bpy.ops.constraint.objectsolver_clear_inverse(context, constraint='Object Solver')
|
|
self.matrix_test('Object Solver.owner', initial_matrix)
|
|
|
|
|
|
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()
|