This commit takes the 'Slotted Actions' out of the experimental phase. As a result: - All newly created Actions will be slotted Actions. - Legacy Actions loaded from disk will be versioned to slotted Actions. - The new Python API for slots, layers, strips, and channel bags is available. - The legacy Python API for accessing F-Curves and Action Groups is still available, and will operate on the F-Curves/Groups for the first slot only. - Creating an Action by keying (via the UI, operators, or the `rna_struct.keyframe_insert` function) will try and share Actions between related data-blocks. See !126655 for more info about this. - Assigning an Action to a data-block will auto-assign a suitable Action Slot. The logic for this is described below. However, There are cases where this does _not_ automatically assign a slot, and thus the Action will effectively _not_ animate the data-block. Effort has been spent to make Action selection work both reliably for Blender users as well as keep the behaviour the same for Python scripts. Where these two goals did not converge, reliability and understandability for users was prioritised. Auto-selection of the Action Slot upon assigning the Action works as follows. The first rule to find a slot wins. 1. The data-block remembers the slot name that was last assigned. If the newly assigned Action has a slot with that name, it is chosen. 2. If the Action has a slot with the same name as the data-block, it is chosen. 3. If the Action has only one slot, and it has never been assigned to anything, it is chosen. 4. If the Action is assigned to an NLA strip or an Action constraint, and the Action has a single slot, and that slot has a suitable ID type, it is chosen. This last step is what I was referring to with "Where these two goals did not converge, reliability and understandability for users was prioritised." For regular Action assignments (like via the Action selectors in the Properties editor) this rule doesn't apply, even though with legacy Actions the final state ("it is animated by this Action") differs from the final state with slotted Actions ("it has no slot so is not animated"). This is done to support the following workflow: - Create an Action by animating Cube. - In order to animate Suzanne with that same Action, assign the Action to Suzanne. - Start keying Suzanne. This auto-creates and auto-assigns a new slot for Suzanne. If rule 4. above would apply in this case, the 2nd step would automatically select the Cube slot for Suzanne as well, which would immediately overwrite Suzanne's properties with the Cube animation. Technically, this commit: - removes the `WITH_ANIM_BAKLAVA` build flag, - removes the `use_animation_baklava` experimental flag in preferences, - updates the code to properly deal with the fact that empty Actions are now always considered slotted/layered Actions (instead of that relying on the user preference). Note that 'slotted Actions' and 'layered Actions' are the exact same thing, just focusing on different aspects (slot & layers) of the new data model. The "Baklava phase 1" assumptions are still asserted. This means that: - an Action can have zero or one layer, - that layer can have zero or one strip, - that strip must be of type 'keyframe' and be infinite with zero offset. The code to handle legacy Actions is NOT removed in this commit. It will be removed later. For now it's likely better to keep it around as reference to the old behaviour in order to aid in some inevitable bugfixing. Ref: #120406
482 lines
23 KiB
Python
482 lines
23 KiB
Python
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
./blender.bin --background --factory-startup --python tests/python/bl_constraints.py -- --testdir /path/to/tests/data/constraints
|
|
"""
|
|
|
|
import pathlib
|
|
import sys
|
|
import unittest
|
|
|
|
import bpy
|
|
from bpy.types import Constraint
|
|
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_override = self.constraint_context('Child Of', owner_name='Child Of.object.owner')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.constraint_context('Child Of', owner_name='Child Of.object.owner')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.constraint_context('Child Of', owner_name='Child Of.object.owner',)
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.constraint_context('Child Of', owner_name='Child Of.armature.owner')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.bone_constraint_context('Child Of', owner_name='Child Of.bone.owner')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.constraint_context('Child Of', owner_name='Child Of.vertexgroup.owner')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.childof_clear_inverse(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_override = self.constraint_context('Object Solver')
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.objectsolver_set_inverse(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),
|
|
)))
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
bpy.ops.constraint.objectsolver_clear_inverse(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),
|
|
)))
|
|
|
|
|
|
class CopyTransformsTest(AbstractConstraintTests):
|
|
layer_collection = 'Copy Transforms'
|
|
|
|
def test_mix_mode_object(self):
|
|
"""Copy Transforms: all mix modes for objects"""
|
|
constraint = bpy.data.objects["Copy Transforms.object.owner"].constraints["Copy Transforms"]
|
|
|
|
constraint.mix_mode = 'REPLACE'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.7818737626075745, 0.14389121532440186, 0.4845699667930603, -0.017531070858240128),
|
|
(-0.2741589844226837, -0.591389000415802, -1.2397242784500122, -0.08039521425962448),
|
|
(0.04909384995698929, -1.0109175443649292, 0.7942137122154236, 0.1584688276052475),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'BEFORE_FULL'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-1.0791258811950684, -0.021011866629123688, 0.3120136260986328, 0.9082338809967041),
|
|
(0.2128538191318512, -0.3411901891231537, -1.7376484870910645, -0.39762523770332336),
|
|
(-0.03584420680999756, -1.0162957906723022, 0.8004404306411743, -0.9015425443649292),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'BEFORE'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.9952367544174194, -0.03077685832977295, 0.05301344022154808, 0.9082338809967041),
|
|
(-0.013416174799203873, -0.39984768629074097, -1.8665285110473633, -0.39762523770332336),
|
|
(0.03660336509346962, -0.9833710193634033, 0.75728839635849, -0.9015425443649292),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'BEFORE_SPLIT'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.9952367544174194, -0.03077685832977295, 0.05301344022154808, -1.0175310373306274),
|
|
(-0.013416174799203873, -0.39984768629074097, -1.8665285110473633, 0.9196047782897949),
|
|
(0.03660336509346962, -0.9833710193634033, 0.75728839635849, 0.1584688276052475),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'AFTER_FULL'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.8939255475997925, -0.2866469621658325, 0.7563635110855103, -0.964445173740387),
|
|
(-0.09460853785276413, -0.73727947473526, -1.0267245769500732, 0.9622588753700256),
|
|
(0.37042146921157837, -1.1893107891082764, 1.0113294124603271, 0.21314144134521484),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'AFTER'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.9033845067024231, -0.2048732340335846, 0.7542480826377869, -0.964445173740387),
|
|
(-0.1757974475622177, -0.6721230745315552, -1.5190268754959106, 0.9622588753700256),
|
|
(0.38079890608787537, -0.7963172793388367, 1.0880682468414307, 0.21314144134521484),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
constraint.mix_mode = 'AFTER_SPLIT'
|
|
self.matrix_test('Copy Transforms.object.owner', Matrix((
|
|
(-0.9033845067024231, -0.2048732340335846, 0.7542480826377869, -1.0175310373306274),
|
|
(-0.1757974475622177, -0.6721230745315552, -1.5190268754959106, 0.9196047782897949),
|
|
(0.38079890608787537, -0.7963172793388367, 1.0880682468414307, 0.1584688276052475),
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
)))
|
|
|
|
|
|
class ActionConstraintTest(AbstractConstraintTests):
|
|
layer_collection = "Action"
|
|
|
|
def constraint(self) -> Constraint:
|
|
owner = bpy.context.scene.objects["Action.owner"]
|
|
constraint = owner.constraints["Action"]
|
|
return constraint
|
|
|
|
def test_assign_action_slot_virgin(self):
|
|
action = bpy.data.actions.new("Slotted")
|
|
slot = action.slots.new()
|
|
|
|
con = self.constraint()
|
|
con.action = action
|
|
|
|
self.assertEqual(
|
|
slot,
|
|
con.action_slot,
|
|
"Assigning an Action with a virgin slot should automatically select that slot")
|
|
|
|
|
|
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()
|