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