From 01918486718696f7e57694e95f3894535aa939bc Mon Sep 17 00:00:00 2001 From: Christoph Lendenfeld Date: Thu, 17 Jul 2025 11:05:31 +0200 Subject: [PATCH] Fix #141909: Creating a pose assets captures unkeyed custom properties This was an oversight caused by 358a0479e86268607541fa8dc00336eb1d5a0e4d Before this, only keyed custom properties were capture into the pose asset. This behavior is now restored. Pull Request: https://projects.blender.org/blender/blender/pulls/141937 --- .../editors/animation/anim_asset_ops.cc | 19 +++++++ tests/python/bl_pose_assets.py | 53 +++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/source/blender/editors/animation/anim_asset_ops.cc b/source/blender/editors/animation/anim_asset_ops.cc index e8c9a2d675c..418d8cd5397 100644 --- a/source/blender/editors/animation/anim_asset_ops.cc +++ b/source/blender/editors/animation/anim_asset_ops.cc @@ -122,6 +122,19 @@ static blender::animrig::Action &extract_pose(Main &bmain, BLI_assert(pose_object->pose); Slot &slot = action.slot_add_for_id(pose_object->id); const bArmature *armature = static_cast(pose_object->data); + + Set existing_paths; + if (pose_object->adt && pose_object->adt->action && + pose_object->adt->slot_handle != Slot::unassigned) + { + Action &pose_object_action = pose_object->adt->action->wrap(); + const slot_handle_t pose_object_slot = pose_object->adt->slot_handle; + foreach_fcurve_in_action_slot(pose_object_action, pose_object_slot, [&](FCurve &fcurve) { + RNAPath existing_path = {fcurve.rna_path, std::nullopt, fcurve.array_index}; + existing_paths.add(existing_path); + }); + } + LISTBASE_FOREACH (bPoseChannel *, pose_bone, &pose_object->pose->chanbase) { if (!(pose_bone->bone->flag & BONE_SELECTED) || !blender::animrig::bone_is_visible(armature, pose_bone->bone)) @@ -147,6 +160,12 @@ static blender::animrig::Action &extract_pose(Main &bmain, continue; } for (const int i : values.index_range()) { + if (RNA_property_is_idprop(resolved_property) && + !existing_paths.contains({rna_path_id_to_prop.value(), std::nullopt, i})) + { + /* Skipping custom properties without animation. */ + continue; + } strip_data.keyframe_insert( &bmain, slot, {rna_path_id_to_prop.value(), i}, {1, values[i]}, key_settings); } diff --git a/tests/python/bl_pose_assets.py b/tests/python/bl_pose_assets.py index da7d435fa10..e558a9f36fe 100644 --- a/tests/python/bl_pose_assets.py +++ b/tests/python/bl_pose_assets.py @@ -85,14 +85,18 @@ class CreateAssetTest(unittest.TestCase): self._armature_object.pose.bones[_BONE_NAME_1].bone.select = True self._armature_object.pose.bones[_BONE_NAME_2].bone.select = False - self.assertEqual(len(bpy.data.actions), 0) + self._armature_object.pose.bones[_BONE_NAME_1].keyframe_insert('["bool_test"]') + self._armature_object.pose.bones[_BONE_NAME_1].keyframe_insert('["float_test"]') + + # There is an action for the custom properties. + self.assertEqual(len(bpy.data.actions), 1) bpy.ops.poselib.create_pose_asset( pose_name="local_asset", asset_library_reference='LOCAL', catalog_path="unit_test") - self.assertEqual(len(bpy.data.actions), 1, "Local poses should be stored as actions") - pose_action = bpy.data.actions[0] + self.assertEqual(len(bpy.data.actions), 2, "Local poses should be stored as actions") + pose_action = bpy.data.actions[1] self.assertTrue(pose_action.asset_data is not None, "The created action should be marked as an asset") expected_pose_values = { @@ -122,13 +126,17 @@ class CreateAssetTest(unittest.TestCase): self._armature_object.pose.bones[_BONE_NAME_1].bone.select = True self._armature_object.pose.bones[_BONE_NAME_2].bone.select = False - self.assertEqual(len(bpy.data.actions), 0) + self._armature_object.pose.bones[_BONE_NAME_1].keyframe_insert('["bool_test"]') + self._armature_object.pose.bones[_BONE_NAME_1].keyframe_insert('["float_test"]') + + # There is an action for the custom properties. + self.assertEqual(len(bpy.data.actions), 1) bpy.ops.poselib.create_pose_asset( pose_name="local_asset", asset_library_reference=_LIB_NAME, catalog_path="unit_test") - self.assertEqual(len(bpy.data.actions), 0, "The asset should not have been created in this file") + self.assertEqual(len(bpy.data.actions), 1, "The asset should not have been created in this file") actions_folder = os.path.join(self._library.path, "Saved", "Actions") asset_files = os.listdir(actions_folder) self.assertEqual(len(asset_files), @@ -160,6 +168,41 @@ class CreateAssetTest(unittest.TestCase): self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, expected_pose_values[fcurve.data_path][fcurve.array_index], 4) + def test_custom_properties_without_keys(self): + # Custom properties without keys should not be added to the pose asset. + self._armature_object.pose.bones[_BONE_NAME_1].location = (1, 1, 2) + self._armature_object.pose.bones[_BONE_NAME_2].location = (-1, 0, 0) + + self._armature_object.pose.bones[_BONE_NAME_1].bone.select = True + self._armature_object.pose.bones[_BONE_NAME_2].bone.select = False + + self.assertEqual(len(bpy.data.actions), 0) + bpy.ops.poselib.create_pose_asset( + pose_name="local_asset", + asset_library_reference='LOCAL', + catalog_path="unit_test") + + pose_action = bpy.data.actions[0] + self.assertTrue(pose_action.asset_data is not None, "The created action should be marked as an asset") + + expected_pose_values = { + f'pose.bones["{_BONE_NAME_1}"].location': (1, 1, 2), + f'pose.bones["{_BONE_NAME_1}"].rotation_quaternion': (1, 0, 0, 0), + f'pose.bones["{_BONE_NAME_1}"].scale': (1, 1, 1), + + # The custom properties are not keyed, thus they should not be in the pose asset. + } + expected_pose_values.update(_BBONE_VALUES) + self.assertEqual(len(pose_action.fcurves), 24) + for fcurve in pose_action.fcurves: + self.assertTrue( + fcurve.data_path in expected_pose_values, + "Only the selected bone should be in the pose asset") + self.assertEqual(len(fcurve.keyframe_points), 1, "Only one key should have been created") + self.assertEqual(fcurve.keyframe_points[0].co.x, 1, "Poses should be on the first frame") + self.assertAlmostEqual(fcurve.keyframe_points[0].co.y, + expected_pose_values[fcurve.data_path][fcurve.array_index], 4) + def main(): global args