2023-08-16 00:20:26 +10:00
|
|
|
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
2023-06-15 13:09:04 +10:00
|
|
|
#
|
2022-02-11 09:07:11 +11:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
2020-02-25 12:16:34 +01:00
|
|
|
|
|
|
|
|
"""
|
2025-05-05 15:10:22 +02:00
|
|
|
./blender.bin --background --factory-startup --python tests/python/bl_constraints.py -- --testdir /path/to/tests/files/constraints
|
2020-02-25 12:16:34 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pathlib
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
import bpy
|
Anim: update slot selection for Action Constraint
Like with NLA strips, Action assignment on Action Constraints needs to
have an extra step (compared to regular assignment to animated
data-blocks).
For the Action Constraint, the auto slot selection gets one more
fallback option (compared to the generic code). This is to support the
following scenario, which used to be necessary as a workaround for a bug
in Blender (#127976):
- Python script creates an Action,
- assigns it to the animated object,
- unassigns it from that object,
- and assigns it to the object's Action Constraint.
The generic code doesn't work for this. The first assignment would see
the slot `XXSlot`, and because it has never been used, just use it. This
would change its name to `OBSlot`. The assignment to the Action
Constraint would not see a 'virgin' slot, and thus not auto-select
`OBSlot`. This behaviour makes sense when assigning Actions in the
Action editor (it shouldn't automatically pick the first slot of
matching ID type), but for the Action Constraint I (Sybren) feel that it
could be a bit more 'enthousiastic' in auto-picking a slot.
Note that this is the same behaviour as for NLA strips, albeit for a
slightly different reason. Because of that it's not sharing code with
the NLA.
Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 15:29:31 +02:00
|
|
|
from bpy.types import Constraint
|
2020-02-25 12:16:34 +01:00
|
|
|
from mathutils import Matrix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AbstractConstraintTests(unittest.TestCase):
|
|
|
|
|
"""Useful functionality for constraint tests."""
|
|
|
|
|
|
2020-02-25 16:46:37 +01:00
|
|
|
layer_collection = '' # When set, this layer collection will be enabled.
|
|
|
|
|
|
2020-02-25 12:16:34 +01:00
|
|
|
def setUp(self):
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "constraints.blend"))
|
|
|
|
|
|
2020-02-25 16:46:37 +01:00
|
|
|
# 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
|
|
|
|
|
|
2020-09-24 18:29:39 +02:00
|
|
|
def assert_matrix(self, actual_matrix, expect_matrix, object_name: str, places=None, delta=1e-6):
|
2020-02-25 12:16:34 +01:00
|
|
|
"""Asserts that the matrices almost equal."""
|
2020-02-25 14:28:33 +01:00
|
|
|
self.assertEqual(len(actual_matrix), 4, 'Expected a 4x4 matrix')
|
2020-02-25 12:16:34 +01:00
|
|
|
|
|
|
|
|
# TODO(Sybren): decompose the matrices and compare loc, rot, and scale separately.
|
|
|
|
|
# That'll probably improve readability & understandability of test failures.
|
2020-02-25 14:28:33 +01:00
|
|
|
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}]')
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-05-25 15:34:54 +02:00
|
|
|
def _get_eval_object(self, object_name: str) -> bpy.types.Object:
|
|
|
|
|
"""Return the evaluated object."""
|
2020-02-25 12:16:34 +01:00
|
|
|
depsgraph = bpy.context.view_layer.depsgraph
|
|
|
|
|
depsgraph.update()
|
|
|
|
|
ob_orig = bpy.context.scene.objects[object_name]
|
|
|
|
|
ob_eval = ob_orig.evaluated_get(depsgraph)
|
2020-05-25 15:34:54 +02:00
|
|
|
return ob_eval
|
|
|
|
|
|
|
|
|
|
def matrix(self, object_name: str) -> Matrix:
|
|
|
|
|
"""Return the evaluated world matrix."""
|
|
|
|
|
ob_eval = self._get_eval_object(object_name)
|
2020-02-25 12:16:34 +01:00
|
|
|
return ob_eval.matrix_world
|
|
|
|
|
|
2020-05-25 15:34:54 +02:00
|
|
|
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
|
|
|
|
|
|
2020-02-25 12:16:34 +01:00
|
|
|
def matrix_test(self, object_name: str, expect: Matrix):
|
|
|
|
|
"""Assert that the object's world matrix is as expected."""
|
|
|
|
|
actual = self.matrix(object_name)
|
2020-02-25 14:28:33 +01:00
|
|
|
self.assert_matrix(actual, expect, object_name)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-05-25 15:34:54 +02:00
|
|
|
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)
|
|
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
def constraint_context(self, constraint_name: str, owner_name: str = '') -> dict:
|
2020-05-25 15:34:54 +02:00
|
|
|
"""Return a context suitable for calling object constraint operators.
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
Assumes the owner is called "{constraint_name}.owner" if owner_name=''.
|
2020-02-25 12:16:34 +01:00
|
|
|
"""
|
2020-02-25 14:28:33 +01:00
|
|
|
owner = bpy.context.scene.objects[owner_name or f'{constraint_name}.owner']
|
2020-02-25 12:16:34 +01:00
|
|
|
constraint = owner.constraints[constraint_name]
|
|
|
|
|
context = {
|
|
|
|
|
**bpy.context.copy(),
|
2020-02-25 14:28:33 +01:00
|
|
|
'object': owner,
|
|
|
|
|
'active_object': owner,
|
2020-02-25 12:16:34 +01:00
|
|
|
'constraint': constraint,
|
|
|
|
|
}
|
|
|
|
|
return context
|
|
|
|
|
|
2020-05-25 15:34:54 +02:00
|
|
|
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
|
|
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
|
2020-02-25 12:16:34 +01:00
|
|
|
class ChildOfTest(AbstractConstraintTests):
|
2020-02-25 16:46:37 +01:00
|
|
|
layer_collection = 'Child Of'
|
|
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
def test_object_simple_parent(self):
|
|
|
|
|
"""Child Of: simple evaluation of object parent."""
|
2020-02-25 12:16:34 +01:00
|
|
|
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),
|
|
|
|
|
))
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
2020-02-25 12:16:34 +01:00
|
|
|
(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),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
def test_object_rotation_only(self):
|
2020-02-25 12:16:34 +01:00
|
|
|
"""Child Of: rotation only."""
|
2020-02-25 14:28:33 +01:00
|
|
|
owner = bpy.context.scene.objects['Child Of.object.owner']
|
2020-02-25 12:16:34 +01:00
|
|
|
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),
|
|
|
|
|
))
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
2020-02-25 12:16:34 +01:00
|
|
|
(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),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-02-25 14:28:33 +01:00
|
|
|
def test_object_no_x_axis(self):
|
2020-02-25 12:16:34 +01:00
|
|
|
"""Child Of: loc/rot/scale on only Y and Z axes."""
|
2020-02-25 14:28:33 +01:00
|
|
|
owner = bpy.context.scene.objects['Child Of.object.owner']
|
2020-02-25 12:16:34 +01:00
|
|
|
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),
|
|
|
|
|
))
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.object.owner', Matrix((
|
Constraints: replace 'Set Inverse' operator with an eval-time update
This fixes {T70269}.
Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.
This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.
This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.
The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.
This patch was original written by @angavrilov and subsequently updated
by me.
Differential Revision: https://developer.blender.org/D6091
2020-02-27 10:24:11 +01:00
|
|
|
(0.9992386102676392, 0.019843991845846176, -0.03359176218509674, 0.10000000149011612),
|
|
|
|
|
(-0.017441775649785995, 0.997369647026062, 0.0703534483909607, 0.2000001221895218),
|
|
|
|
|
(0.034899499267339706, -0.06971398741006851, 0.996956467628479, 0.3000001311302185),
|
2020-02-25 12:16:34 +01:00
|
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of')
|
2020-02-25 14:28:33 +01:00
|
|
|
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)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
2020-02-25 14:28:33 +01:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of')
|
2020-02-25 14:28:33 +01:00
|
|
|
self.matrix_test('Child Of.armature.owner', initial_matrix)
|
2020-02-25 12:16:34 +01:00
|
|
|
|
2020-05-25 15:34:54 +02:00
|
|
|
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)
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
2020-05-25 15:34:54 +02:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of', owner='BONE')
|
2020-05-25 15:34:54 +02:00
|
|
|
self.bone_matrix_test('Child Of.bone.owner', 'Child Of.bone', initial_matrix)
|
|
|
|
|
|
Constraints: replace 'Set Inverse' operator with an eval-time update
This fixes {T70269}.
Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.
This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.
This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.
The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.
This patch was original written by @angavrilov and subsequently updated
by me.
Differential Revision: https://developer.blender.org/D6091
2020-02-27 10:24:11 +01:00
|
|
|
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)
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
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')
|
Constraints: replace 'Set Inverse' operator with an eval-time update
This fixes {T70269}.
Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.
This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.
This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.
The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.
This patch was original written by @angavrilov and subsequently updated
by me.
Differential Revision: https://developer.blender.org/D6091
2020-02-27 10:24:11 +01:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.childof_clear_inverse(constraint='Child Of')
|
Constraints: replace 'Set Inverse' operator with an eval-time update
This fixes {T70269}.
Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.
This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.
This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.
The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.
This patch was original written by @angavrilov and subsequently updated
by me.
Differential Revision: https://developer.blender.org/D6091
2020-02-27 10:24:11 +01:00
|
|
|
self.matrix_test('Child Of.vertexgroup.owner', initial_matrix)
|
|
|
|
|
|
2020-02-25 15:44:21 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
context_override = self.constraint_context('Object Solver')
|
|
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.objectsolver_set_inverse(constraint='Object Solver')
|
2020-02-25 15:44:21 +01:00
|
|
|
self.matrix_test('Object Solver.owner', Matrix((
|
Constraints: replace 'Set Inverse' operator with an eval-time update
This fixes {T70269}.
Before this commit there was complicated code to try and compute the
correct parent inverse matrix for the 'Child Of' and 'Object Solver'
constraints outside the constraint evaluation. This was done mostly
correctly, but did have some issues. The Set Inverse operator now defers
this computation to be performed during constraint evaluation by just
setting a flag. If the constraint is disabled, and thus tagging it for
update in the depsgraph is not enough to trigger immediate evaluation,
evaluation is forced by temporarily enabling it.
This fix changes the way how the inverse matrix works when some of the
channels of the constraint are disabled. Before this commit, the channel
flags were used to filter both the parent and the inverse matrix. This
meant that it was impossible to make an inverse matrix that would
actually fully neutralize the effect of the constraint. Now only the
parent matrix is filtered, while inverse is applied fully. As a result,
pressing the 'Set Inverse' matrix produces the same transformation as
disabling the constraint. This is also reflected in the changed values
in the 'Child Of' unit test.
This change is not backward compatible, but it should be OK because the
old way was effectively unusable, so it is unlikely anybody relied on
it.
The change in matrix for the Object Solver constraint is due to a
different method of computing it, which caused a slightly different
floating point error that was slightly bigger than allowed by the test,
so I updated the matrix values there as well.
This patch was original written by @angavrilov and subsequently updated
by me.
Differential Revision: https://developer.blender.org/D6091
2020-02-27 10:24:11 +01:00
|
|
|
(0.9992387294769287, 0.019843989983201027, -0.03359176591038704, 0.10000025480985641),
|
|
|
|
|
(-0.017441747710108757, 0.9973697662353516, 0.07035345584154129, 0.1999993920326233),
|
|
|
|
|
(0.034899502992630005, -0.06971398741006851, 0.996956467628479, 0.29999980330467224),
|
2020-02-25 15:44:21 +01:00
|
|
|
(0.0, 0.0, 0.0, 1.0),
|
|
|
|
|
)))
|
|
|
|
|
|
2023-05-23 14:34:09 +10:00
|
|
|
with bpy.context.temp_override(**context_override):
|
|
|
|
|
bpy.ops.constraint.objectsolver_clear_inverse(constraint='Object Solver')
|
2020-02-25 15:44:21 +01:00
|
|
|
self.matrix_test('Object Solver.owner', initial_matrix)
|
|
|
|
|
|
|
|
|
|
|
2020-12-03 10:42:29 +01:00
|
|
|
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),
|
|
|
|
|
)))
|
|
|
|
|
|
|
|
|
|
|
2020-11-04 19:29:27 +03:00
|
|
|
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)
|
|
|
|
|
)))
|
|
|
|
|
|
|
|
|
|
|
Anim: update slot selection for Action Constraint
Like with NLA strips, Action assignment on Action Constraints needs to
have an extra step (compared to regular assignment to animated
data-blocks).
For the Action Constraint, the auto slot selection gets one more
fallback option (compared to the generic code). This is to support the
following scenario, which used to be necessary as a workaround for a bug
in Blender (#127976):
- Python script creates an Action,
- assigns it to the animated object,
- unassigns it from that object,
- and assigns it to the object's Action Constraint.
The generic code doesn't work for this. The first assignment would see
the slot `XXSlot`, and because it has never been used, just use it. This
would change its name to `OBSlot`. The assignment to the Action
Constraint would not see a 'virgin' slot, and thus not auto-select
`OBSlot`. This behaviour makes sense when assigning Actions in the
Action editor (it shouldn't automatically pick the first slot of
matching ID type), but for the Action Constraint I (Sybren) feel that it
could be a bit more 'enthousiastic' in auto-picking a slot.
Note that this is the same behaviour as for NLA strips, albeit for a
slightly different reason. Because of that it's not sharing code with
the NLA.
Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 15:29:31 +02:00
|
|
|
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")
|
2024-12-02 17:04:37 +01:00
|
|
|
slot = action.slots.new('OBJECT', "Slot")
|
Anim: update slot selection for Action Constraint
Like with NLA strips, Action assignment on Action Constraints needs to
have an extra step (compared to regular assignment to animated
data-blocks).
For the Action Constraint, the auto slot selection gets one more
fallback option (compared to the generic code). This is to support the
following scenario, which used to be necessary as a workaround for a bug
in Blender (#127976):
- Python script creates an Action,
- assigns it to the animated object,
- unassigns it from that object,
- and assigns it to the object's Action Constraint.
The generic code doesn't work for this. The first assignment would see
the slot `XXSlot`, and because it has never been used, just use it. This
would change its name to `OBSlot`. The assignment to the Action
Constraint would not see a 'virgin' slot, and thus not auto-select
`OBSlot`. This behaviour makes sense when assigning Actions in the
Action editor (it shouldn't automatically pick the first slot of
matching ID type), but for the Action Constraint I (Sybren) feel that it
could be a bit more 'enthousiastic' in auto-picking a slot.
Note that this is the same behaviour as for NLA strips, albeit for a
slightly different reason. Because of that it's not sharing code with
the NLA.
Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 15:29:31 +02:00
|
|
|
|
|
|
|
|
con = self.constraint()
|
|
|
|
|
con.action = action
|
|
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
slot,
|
|
|
|
|
con.action_slot,
|
|
|
|
|
"Assigning an Action with a virgin slot should automatically select that slot")
|
|
|
|
|
|
2025-05-15 14:30:01 +02:00
|
|
|
def test_mix_modes(self):
|
|
|
|
|
owner = bpy.context.scene.objects["Action.owner"]
|
|
|
|
|
target = bpy.context.scene.objects["Action.target"]
|
|
|
|
|
|
|
|
|
|
action = bpy.data.actions.new("Slotted")
|
|
|
|
|
slot = action.slots.new('OBJECT', "Slot")
|
|
|
|
|
layer = action.layers.new(name="Layer")
|
|
|
|
|
strip = layer.strips.new(type='KEYFRAME')
|
|
|
|
|
strip.key_insert(slot, "location", 0, 2.0, 0.0)
|
|
|
|
|
strip.key_insert(slot, "location", 0, 7.0, 10.0)
|
|
|
|
|
|
|
|
|
|
con = owner.constraints["Action"]
|
|
|
|
|
con.action = action
|
|
|
|
|
con.action_slot = slot
|
|
|
|
|
con.transform_channel = 'LOCATION_X'
|
|
|
|
|
con.min = -1.0
|
|
|
|
|
con.max = 1.0
|
|
|
|
|
con.frame_start = 0
|
|
|
|
|
con.frame_end = 10
|
|
|
|
|
|
|
|
|
|
# Set the constrained object's location to something other than [0,0,0],
|
|
|
|
|
# so we can verify that it's actually replaced/mixed as appropriate to
|
|
|
|
|
# the mix mode.
|
|
|
|
|
owner.location = (2.0, 3.0, 4.0)
|
|
|
|
|
|
|
|
|
|
con.mix_mode = 'REPLACE'
|
|
|
|
|
self.matrix_test("Action.owner", Matrix((
|
|
|
|
|
(1.0, 0.0, 0.0, 4.5),
|
|
|
|
|
(0.0, 1.0, 0.0, 0.0),
|
|
|
|
|
(0.0, 0.0, 1.0, 0.0),
|
|
|
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
|
|
|
)))
|
|
|
|
|
|
|
|
|
|
con.mix_mode = 'BEFORE_SPLIT'
|
|
|
|
|
self.matrix_test("Action.owner", Matrix((
|
|
|
|
|
(1.0, 0.0, 0.0, 6.5),
|
|
|
|
|
(0.0, 1.0, 0.0, 3.0),
|
|
|
|
|
(0.0, 0.0, 1.0, 4.0),
|
|
|
|
|
(0.0, 0.0, 0.0, 1.0)
|
|
|
|
|
)))
|
|
|
|
|
|
Anim: update slot selection for Action Constraint
Like with NLA strips, Action assignment on Action Constraints needs to
have an extra step (compared to regular assignment to animated
data-blocks).
For the Action Constraint, the auto slot selection gets one more
fallback option (compared to the generic code). This is to support the
following scenario, which used to be necessary as a workaround for a bug
in Blender (#127976):
- Python script creates an Action,
- assigns it to the animated object,
- unassigns it from that object,
- and assigns it to the object's Action Constraint.
The generic code doesn't work for this. The first assignment would see
the slot `XXSlot`, and because it has never been used, just use it. This
would change its name to `OBSlot`. The assignment to the Action
Constraint would not see a 'virgin' slot, and thus not auto-select
`OBSlot`. This behaviour makes sense when assigning Actions in the
Action editor (it shouldn't automatically pick the first slot of
matching ID type), but for the Action Constraint I (Sybren) feel that it
could be a bit more 'enthousiastic' in auto-picking a slot.
Note that this is the same behaviour as for NLA strips, albeit for a
slightly different reason. Because of that it's not sharing code with
the NLA.
Pull Request: https://projects.blender.org/blender/blender/pulls/128892
2024-10-11 15:29:31 +02:00
|
|
|
|
2020-02-25 12:16:34 +01:00
|
|
|
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__":
|
2020-04-27 17:43:47 +02:00
|
|
|
main()
|