168 lines
7.5 KiB
Python
168 lines
7.5 KiB
Python
# SPDX-FileCopyrightText: 2026 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
import unittest
|
|
import bpy
|
|
import pathlib
|
|
import sys
|
|
|
|
|
|
_BONE_NAME_A = "bone_a"
|
|
_BONE_NAME_B = "bone_b"
|
|
|
|
|
|
def _set_up_driver(driver: bpy.types.Driver, target_id: bpy.types.ID, target_path: str) -> None:
|
|
driver_variable_name = "var"
|
|
|
|
driver.type = 'AVERAGE'
|
|
driver.expression = driver_variable_name
|
|
|
|
driver_var = driver.variables.new()
|
|
driver_var.name = driver_variable_name
|
|
driver_var.type = 'SINGLE_PROP'
|
|
|
|
driver_target = driver_var.targets[0]
|
|
driver_target.id_type = target_id.id_type
|
|
driver_target.id = target_id
|
|
driver_target.data_path = target_path
|
|
|
|
|
|
class PoseBoneRenameTest(unittest.TestCase):
|
|
"""Ensure that drivers and animation are moved to the correct RNA path once a pose bone is renamed."""
|
|
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
|
|
|
armature = bpy.data.armatures.new("test_armature")
|
|
self.armature_obj = bpy.data.objects.new("test_armature_object", armature)
|
|
bpy.context.scene.collection.objects.link(self.armature_obj)
|
|
bpy.context.view_layer.objects.active = self.armature_obj
|
|
self.armature_obj.select_set(True)
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
edit_bone = armature.edit_bones.new(_BONE_NAME_A)
|
|
edit_bone.head = (1, 0, 0)
|
|
edit_bone.tail = (0, 0, 0)
|
|
|
|
edit_bone = armature.edit_bones.new(_BONE_NAME_B)
|
|
edit_bone.head = (1, 1, 0)
|
|
edit_bone.tail = (0, 1, 0)
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
# This is useful to check if the rename does not affect animation on identically named
|
|
# bones in a different object.
|
|
self.armature_obj_dupli = self.armature_obj.copy()
|
|
self.armature_obj_dupli.data = armature.copy()
|
|
bpy.context.scene.collection.objects.link(self.armature_obj_dupli)
|
|
|
|
def test_rename_bone_driver(self) -> None:
|
|
fcu = self.armature_obj.data.bones[_BONE_NAME_A].driver_add("bbone_segments", -1)
|
|
self.assertEqual(fcu.data_path, f'bones["{_BONE_NAME_A}"].bbone_segments')
|
|
_set_up_driver(fcu.driver, self.armature_obj, f'pose.bones["{_BONE_NAME_B}"].location[0]')
|
|
|
|
# Setting up the same driver on the duplicate armature.
|
|
fcu_dupli = self.armature_obj_dupli.data.bones[_BONE_NAME_A].driver_add("bbone_segments", -1)
|
|
self.assertEqual(fcu_dupli.data_path, fcu.data_path)
|
|
_set_up_driver(fcu_dupli.driver, self.armature_obj_dupli, f'pose.bones["{_BONE_NAME_B}"].location[0]')
|
|
|
|
bone_a_new_name = "bone_a_2"
|
|
self.armature_obj.pose.bones[_BONE_NAME_A].name = bone_a_new_name
|
|
|
|
# `bbone_segments` is a property of the bone thus armature.
|
|
self.assertEqual(len(self.armature_obj.data.animation_data.drivers), 1, "Shouldn't remove the driver")
|
|
self.assertEqual(fcu.data_path, f'bones["{bone_a_new_name}"].bbone_segments')
|
|
|
|
self.assertEqual(
|
|
fcu_dupli.data_path,
|
|
f'bones["{_BONE_NAME_A}"].bbone_segments',
|
|
"Should not modify duplicate armature")
|
|
|
|
bone_b_new_name = "bone_b_2"
|
|
self.armature_obj.pose.bones[_BONE_NAME_B].name = bone_b_new_name
|
|
|
|
# Testing if driver targets are still correct after a rename.
|
|
# TODO: this currently does not work!
|
|
# driver_target = fcu.driver.variables[0].targets[0]
|
|
# self.assertEqual(driver_target.data_path, f"pose.bones[\"{bone_b_new_name}\"].location[0]")
|
|
|
|
def test_rename_pose_bone_driver(self) -> None:
|
|
fcu = self.armature_obj.pose.bones[_BONE_NAME_A].driver_add("hide", -1)
|
|
self.assertEqual(fcu.data_path, f'pose.bones["{_BONE_NAME_A}"].hide')
|
|
_set_up_driver(fcu.driver, self.armature_obj, f'pose.bones["{_BONE_NAME_B}"].location[0]')
|
|
|
|
# Setting up the same driver on the duplicate armature.
|
|
fcu_dupli = self.armature_obj_dupli.pose.bones[_BONE_NAME_A].driver_add("hide", -1)
|
|
self.assertEqual(fcu_dupli.data_path, fcu.data_path)
|
|
_set_up_driver(fcu_dupli.driver, self.armature_obj_dupli, f'pose.bones["{_BONE_NAME_B}"].location[0]')
|
|
|
|
bone_a_new_name = "bone_a_2"
|
|
self.armature_obj.pose.bones[_BONE_NAME_A].name = bone_a_new_name
|
|
|
|
# `hide` is a property on the pose bone thus object.
|
|
self.assertEqual(len(self.armature_obj.animation_data.drivers), 1, "Shouldn't remove the driver")
|
|
self.assertEqual(fcu.data_path, f'pose.bones["{bone_a_new_name}"].hide')
|
|
|
|
self.assertEqual(
|
|
fcu_dupli.data_path,
|
|
f'pose.bones["{_BONE_NAME_A}"].hide',
|
|
"Should not modify the duplicate armature")
|
|
|
|
bone_b_new_name = "bone_b_2"
|
|
self.armature_obj.pose.bones[_BONE_NAME_B].name = bone_b_new_name
|
|
|
|
# Testing if driver targets are still correct after a rename.
|
|
# TODO: this currently does not work!
|
|
# driver_target = fcu.driver.variables[0].targets[0]
|
|
# self.assertEqual(driver_target.data_path, f"pose.bones[\"{bone_b_new_name}\"].location[0]")
|
|
|
|
def test_rename_bone_animation(self) -> None:
|
|
# Not particularly useful to animate this property, but it can be done, so better test it.
|
|
self.armature_obj.data.bones[_BONE_NAME_A].keyframe_insert("bbone_segments")
|
|
self.assertNotEqual(self.armature_obj.data.animation_data.action, None)
|
|
action = self.armature_obj.data.animation_data.action
|
|
fcurves = action.layers[0].strips[0].channelbags[0].fcurves
|
|
self.assertEqual(fcurves[0].data_path, f'bones[\"{_BONE_NAME_A}"].bbone_segments')
|
|
|
|
self.armature_obj_dupli.data.bones[_BONE_NAME_A].keyframe_insert("bbone_segments")
|
|
self.assertNotEqual(self.armature_obj_dupli.data.animation_data.action, None)
|
|
|
|
bone_a_new_name = "bone_a_2"
|
|
# Renaming the pose bone also renames the bone itself, which should rename the fcurve data path too.
|
|
self.armature_obj.pose.bones[_BONE_NAME_A].name = bone_a_new_name
|
|
|
|
self.assertEqual(fcurves[0].data_path, f'bones[\"{bone_a_new_name}"].bbone_segments')
|
|
|
|
dupli_action = self.armature_obj_dupli.data.animation_data.action
|
|
dupli_fcurves = dupli_action.layers[0].strips[0].channelbags[0].fcurves
|
|
self.assertEqual(dupli_fcurves[0].data_path, f'bones[\"{_BONE_NAME_A}"].bbone_segments')
|
|
|
|
def test_rename_pose_bone_animation(self) -> None:
|
|
self.armature_obj.pose.bones[_BONE_NAME_A].keyframe_insert("location")
|
|
self.assertNotEqual(self.armature_obj.animation_data.action, None)
|
|
action = self.armature_obj.animation_data.action
|
|
fcurves = action.layers[0].strips[0].channelbags[0].fcurves
|
|
for fcurve in fcurves:
|
|
self.assertEqual(fcurve.data_path, f'pose.bones[\"{_BONE_NAME_A}"].location')
|
|
|
|
self.armature_obj_dupli.pose.bones[_BONE_NAME_A].keyframe_insert("location")
|
|
self.assertNotEqual(self.armature_obj_dupli.animation_data.action, None)
|
|
|
|
bone_a_new_name = "bone_a_2"
|
|
self.armature_obj.pose.bones[_BONE_NAME_A].name = bone_a_new_name
|
|
|
|
for fcurve in fcurves:
|
|
self.assertEqual(fcurve.data_path, f'pose.bones[\"{bone_a_new_name}"].location')
|
|
|
|
dupli_action = self.armature_obj_dupli.animation_data.action
|
|
for fcurve in dupli_action.layers[0].strips[0].channelbags[0].fcurves:
|
|
self.assertEqual(fcurve.data_path, f'pose.bones[\"{_BONE_NAME_A}"].location')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
|
unittest.main()
|