blender/tests/python/bl_constraints.py
Henrik Dick a6c4e39876 Add Custom Object Space to Constraints
Add Custom Space to the list of space conversions for constraints.

Constraints can use World Space, Local Space, Pose Space, Local with
Parent, and now also Custom Space with a custom object to define the
evaluation space.

The Custom Space option uses the Local Space of an other
object/bone/vertex group. If selected on owner or target it will show a
box for object selection. If an armature is selected, then it will also
show a box for bone selection. If a mesh object is selected it will show
the option for using the local space of a vertex group.

Reviewed By: #animation_rigging, sybren, Severin, angavrilov

Differential Revision: https://developer.blender.org/D7437
2020-12-03 11:20:21 +01:00

396 lines
19 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=None, delta=1e-6):
"""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)
class CustomSpaceTest(AbstractConstraintTests):
layer_collection = 'Custom Space'
def test_loc_like_object(self):
"""Custom Space: basic custom space evaluation for objects"""
loc_like_constraint = bpy.data.objects["Custom Space.object.owner"].constraints["Copy Location"]
loc_like_constraint.use_x = True
loc_like_constraint.use_y = True
loc_like_constraint.use_z = True
self.matrix_test('Custom Space.object.owner', Matrix((
(1.0, 0.0, -2.9802322387695312e-08, -0.01753106713294983),
(0.0, 1.0, 0.0, -0.08039519190788269),
(-2.9802322387695312e-08, 5.960464477539063e-08, 1.0, 0.1584688425064087),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_x = False
self.matrix_test('Custom Space.object.owner', Matrix((
(1.0, 0.0, -2.9802322387695312e-08, 0.18370598554611206),
(0.0, 1.0, 0.0, 0.47120195627212524),
(-2.9802322387695312e-08, 5.960464477539063e-08, 1.0, -0.16521614789962769),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_y = False
self.matrix_test('Custom Space.object.owner', Matrix((
(1.0, 0.0, -2.9802322387695312e-08, -0.46946945786476135),
(0.0, 1.0, 0.0, 0.423120379447937),
(-2.9802322387695312e-08, 5.960464477539063e-08, 1.0, -0.6532361507415771),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_z = False
loc_like_constraint.use_y = True
self.matrix_test('Custom Space.object.owner', Matrix((
(1.0, 0.0, -2.9802322387695312e-08, -0.346824586391449),
(0.0, 1.0, 0.0, 1.0480815172195435),
(-2.9802322387695312e-08, 5.960464477539063e-08, 1.0, 0.48802000284194946),
(0.0, 0.0, 0.0, 1.0),
)))
def test_loc_like_armature(self):
"""Custom Space: basic custom space evaluation for bones"""
loc_like_constraint = bpy.data.objects["Custom Space.armature.owner"].pose.bones["Bone"].constraints["Copy Location"]
loc_like_constraint.use_x = True
loc_like_constraint.use_y = True
loc_like_constraint.use_z = True
self.bone_matrix_test('Custom Space.armature.owner', 'Bone', Matrix((
(0.4556015729904175, -0.03673229366540909, -0.8894257545471191, -0.01753103733062744),
(-0.45956411957740784, -0.8654094934463501, -0.19966775178909302, -0.08039522171020508),
(-0.762383222579956, 0.49971696734428406, -0.4111628830432892, 0.1584688425064087),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_x = False
self.bone_matrix_test('Custom Space.armature.owner', 'Bone', Matrix((
(0.4556015729904175, -0.03673229366540909, -0.8894257545471191, -0.310153603553772),
(-0.45956411957740784, -0.8654094934463501, -0.19966775178909302, -0.8824828863143921),
(-0.762383222579956, 0.49971696734428406, -0.4111628830432892, 0.629145085811615),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_y = False
self.bone_matrix_test('Custom Space.armature.owner', 'Bone', Matrix((
(0.4556015729904175, -0.03673229366540909, -0.8894257545471191, -1.0574829578399658),
(-0.45956411957740784, -0.8654094934463501, -0.19966775178909302, -0.937495231628418),
(-0.762383222579956, 0.49971696734428406, -0.4111628830432892, 0.07077804207801819),
(0.0, 0.0, 0.0, 1.0),
)))
loc_like_constraint.use_z = False
loc_like_constraint.use_y = True
self.bone_matrix_test('Custom Space.armature.owner', 'Bone', Matrix((
(0.4556015729904175, -0.03673229366540909, -0.8894257545471191, -0.25267064571380615),
(-0.45956411957740784, -0.8654094934463501, -0.19966775178909302, -0.9449876546859741),
(-0.762383222579956, 0.49971696734428406, -0.4111628830432892, 0.5583670735359192),
(0.0, 0.0, 0.0, 1.0),
)))
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()