mirror of
https://github.com/blender/blender
synced 2026-10-25 03:07:01 +03:00
Through a convoluted logic the previous AnimData renaming code basically did: * For any given ID * check all FCurves it uses directly for the rename string (includes owned drivers) * check all other AnimData in Main if the given ID is being used as a driver target When doing that in a loop for many IDs, things can slow down considerably. The solution is to build a map of ID to the drivers using it as a target beforehand. That way, we only have to do loop main once to build the map and can then reuse it to subsequent calls to the rename function (which only looks at the given ID) ### Implementation **Removed `BKE_animdata_fix_paths_rename`**: Since this function doesn't check for driver targets by looping main, it missed to rename them leaving invalid drivers behind. When renaming anything that can be animated it is never correct to ignore driver targets which is why I removed it. Replaced with calls to `BKE_animdata_fix_paths`. **Removed `BKE_animdata_fix_paths_rename_all_ex` and `BKE_animdata_fix_paths_rename_all`**: This has been replaced with a combination of `BKE_animdata_build_driver_target_map` and `BKE_animdata_fix_paths`. By splitting this up we can get the performance gains when doing many renames at once. **Added `RNA_path_name_to_infix` and `RNA_path_number_to_infix`**: The new functions no longer assume anything about the rename string they are given. Instead the caller has to format the strings accordingly which is done with this function. Doing so removes a boolean argument from the API. In order to get the un-escaped name (which is needed for drivers targeting pose bones by name) I added `infix_to_name`. This function is not public because it makes a few assumptions about the structure of the given string. **`driver_target_path_fix` no longer verifies the path** RNA paths for drivers can contain a trailing number like `[0]` to indicate the array index. For regular rna paths this is usually a separate integer which is why such paths won't resolve. Verifying the path would mean that driver targets are not correctly updated, as confirmed by the unit test that was disabled. This is now fixed and the unit test was enabled. ### Fixed Issues Fixes #155262 Fixes #159712 Fixes unreported: Drivers targeting VSE strips break if strip is renamed Depending on the file, this is up to 27x times faster. Part of #160849 Pull Request: https://projects.blender.org/blender/blender/pulls/160988
165 lines
7.4 KiB
Python
165 lines
7.4 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.
|
|
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.
|
|
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()
|