221 lines
8.1 KiB
Python
221 lines
8.1 KiB
Python
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import unittest
|
|
import bpy
|
|
import pathlib
|
|
import sys
|
|
from math import radians
|
|
|
|
"""
|
|
blender -b -noaudio --factory-startup --python tests/python/bl_animation_keyframing.py -- --testdir /path/to/lib/tests/animation
|
|
"""
|
|
|
|
|
|
def _fcurve_paths_match(fcurves: list, expected_paths: list) -> bool:
|
|
data_paths = list(set([fcurve.data_path for fcurve in fcurves]))
|
|
return sorted(data_paths) == sorted(expected_paths)
|
|
|
|
|
|
def _get_view3d_context():
|
|
ctx = bpy.context.copy()
|
|
|
|
for area in bpy.context.window.screen.areas:
|
|
if area.type != 'VIEW_3D':
|
|
continue
|
|
|
|
ctx['area'] = area
|
|
ctx['space'] = area.spaces.active
|
|
break
|
|
|
|
return ctx
|
|
|
|
|
|
def _create_animation_object():
|
|
anim_object = bpy.data.objects.new("anim_object", None)
|
|
# Ensure that the rotation mode is correct so we can check against rotation_euler
|
|
anim_object.rotation_mode = "XYZ"
|
|
bpy.context.scene.collection.objects.link(anim_object)
|
|
bpy.context.view_layer.objects.active = anim_object
|
|
anim_object.select_set(True)
|
|
return anim_object
|
|
|
|
|
|
def _insert_by_name_test(insert_key: str, expected_paths: list):
|
|
keyed_object = _create_animation_object()
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert_by_name(type=insert_key)
|
|
match = _fcurve_paths_match(keyed_object.animation_data.action.fcurves, expected_paths)
|
|
bpy.data.objects.remove(keyed_object, do_unlink=True)
|
|
return match
|
|
|
|
|
|
def _get_keying_set(scene, name: str):
|
|
return scene.keying_sets_all[scene.keying_sets_all.find(name)]
|
|
|
|
|
|
def _insert_with_keying_set_test(keying_set_name: str, expected_paths: list):
|
|
scene = bpy.context.scene
|
|
keying_set = _get_keying_set(scene, keying_set_name)
|
|
scene.keying_sets.active = keying_set
|
|
keyed_object = _create_animation_object()
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert()
|
|
match = _fcurve_paths_match(keyed_object.animation_data.action.fcurves, expected_paths)
|
|
bpy.data.objects.remove(keyed_object, do_unlink=True)
|
|
return match
|
|
|
|
|
|
class AbstractKeyframingTest:
|
|
def setUp(self):
|
|
super().setUp()
|
|
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
|
|
|
|
|
class InsertKeyTest(AbstractKeyframingTest, unittest.TestCase):
|
|
""" Ensure keying things by name or with a keying set adds the right keys. """
|
|
|
|
def test_insert_by_name(self):
|
|
self.assertTrue(_insert_by_name_test("Location", ["location"]))
|
|
self.assertTrue(_insert_by_name_test("Rotation", ["rotation_euler"]))
|
|
self.assertTrue(_insert_by_name_test("Scaling", ["scale"]))
|
|
self.assertTrue(_insert_by_name_test("LocRotScale", ["location", "rotation_euler", "scale"]))
|
|
|
|
def test_insert_with_keying_set(self):
|
|
self.assertTrue(_insert_with_keying_set_test("Location", ["location"]))
|
|
self.assertTrue(_insert_with_keying_set_test("Rotation", ["rotation_euler"]))
|
|
self.assertTrue(_insert_with_keying_set_test("Scale", ["scale"]))
|
|
self.assertTrue(
|
|
_insert_with_keying_set_test("Location, Rotation & Scale", ["location", "rotation_euler", "scale"])
|
|
)
|
|
|
|
|
|
class VisualKeyingTest(AbstractKeyframingTest, unittest.TestCase):
|
|
""" Check if visual keying produces the correct keyframe values. """
|
|
|
|
def test_visual_location_keying_set(self):
|
|
t_value = 1
|
|
target = _create_animation_object()
|
|
target.location = (t_value, t_value, t_value)
|
|
constrained = _create_animation_object()
|
|
constraint = constrained.constraints.new("COPY_LOCATION")
|
|
constraint.target = target
|
|
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert_by_name(type="BUILTIN_KSI_VisualLoc")
|
|
|
|
for fcurve in constrained.animation_data.action.fcurves:
|
|
self.assertEqual(fcurve.keyframe_points[0].co.y, t_value)
|
|
|
|
bpy.data.objects.remove(target, do_unlink=True)
|
|
bpy.data.objects.remove(constrained, do_unlink=True)
|
|
|
|
def test_visual_rotation_keying_set(self):
|
|
rot_value_deg = 45
|
|
rot_value_rads = radians(rot_value_deg)
|
|
|
|
target = _create_animation_object()
|
|
target.rotation_euler = (rot_value_rads, rot_value_rads, rot_value_rads)
|
|
constrained = _create_animation_object()
|
|
constraint = constrained.constraints.new("COPY_ROTATION")
|
|
constraint.target = target
|
|
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert_by_name(type="BUILTIN_KSI_VisualRot")
|
|
|
|
for fcurve in constrained.animation_data.action.fcurves:
|
|
self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, rot_value_rads, places=4)
|
|
|
|
bpy.data.objects.remove(target, do_unlink=True)
|
|
bpy.data.objects.remove(constrained, do_unlink=True)
|
|
|
|
def test_visual_location_user_pref(self):
|
|
# When enabling the user preference setting,
|
|
# the normal keying sets behave like their visual keying set counterpart.
|
|
bpy.context.preferences.edit.use_visual_keying = True
|
|
t_value = 1
|
|
target = _create_animation_object()
|
|
target.location = (t_value, t_value, t_value)
|
|
constrained = _create_animation_object()
|
|
constraint = constrained.constraints.new("COPY_LOCATION")
|
|
constraint.target = target
|
|
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert_by_name(type="Location")
|
|
|
|
for fcurve in constrained.animation_data.action.fcurves:
|
|
self.assertEqual(fcurve.keyframe_points[0].co.y, t_value)
|
|
|
|
bpy.data.objects.remove(target, do_unlink=True)
|
|
bpy.data.objects.remove(constrained, do_unlink=True)
|
|
bpy.context.preferences.edit.use_visual_keying = False
|
|
|
|
|
|
class CycleAwareKeyingTest(AbstractKeyframingTest, unittest.TestCase):
|
|
""" Check if cycle aware keying remaps the keyframes correctly and adds fcurve modifiers. """
|
|
|
|
def test_insert_location_cycle_aware(self):
|
|
# In order to make cycle aware keying work, the action needs to be created and have the
|
|
# frame_range set plus the use_frame_range flag set to True.
|
|
keyed_object = _create_animation_object()
|
|
bpy.context.scene.tool_settings.use_keyframe_cycle_aware = True
|
|
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.ops.anim.keyframe_insert_by_name(type="Location")
|
|
|
|
action = keyed_object.animation_data.action
|
|
action.use_cyclic = True
|
|
action.use_frame_range = True
|
|
cyclic_range_end = 20
|
|
action.frame_range = [1, cyclic_range_end]
|
|
|
|
with bpy.context.temp_override(**_get_view3d_context()):
|
|
bpy.context.scene.frame_set(5)
|
|
bpy.ops.anim.keyframe_insert_by_name(type="Location")
|
|
|
|
# Will be mapped to frame 3.
|
|
bpy.context.scene.frame_set(22)
|
|
bpy.ops.anim.keyframe_insert_by_name(type="Location")
|
|
|
|
# Will be mapped to frame 9.
|
|
bpy.context.scene.frame_set(-10)
|
|
bpy.ops.anim.keyframe_insert_by_name(type="Location")
|
|
|
|
# Check that only location keys have been created.
|
|
self.assertTrue(_fcurve_paths_match(action.fcurves, ["location"]))
|
|
|
|
expected_keys = [1, 3, 5, 9, 20]
|
|
|
|
for fcurve in action.fcurves:
|
|
self.assertEqual(len(fcurve.keyframe_points), len(expected_keys))
|
|
key_index = 0
|
|
for key in fcurve.keyframe_points:
|
|
self.assertEqual(key.co.x, expected_keys[key_index])
|
|
key_index += 1
|
|
|
|
# All fcurves should have a cycles modifier.
|
|
self.assertTrue(fcurve.modifiers[0].type == "CYCLES")
|
|
|
|
bpy.data.objects.remove(keyed_object, do_unlink=True)
|
|
|
|
|
|
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()
|