From 566f51c24a5a15916d9290025ac7a149de7967ff Mon Sep 17 00:00:00 2001 From: Christoph Lendenfeld Date: Thu, 27 Feb 2025 14:46:36 +0100 Subject: [PATCH] Fix: selecting bones of pose assets not respecting multiple slots The code for selecting bones from a pose was still using the legacy api, thus it didn't work properly for selecting bones of all slots. Pull Request: https://projects.blender.org/blender/blender/pulls/134912 --- scripts/addons_core/pose_library/operators.py | 4 +- .../addons_core/pose_library/pose_usage.py | 43 ++++++++++++++++--- scripts/modules/bpy_extras/anim_utils.py | 18 +++++--- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/scripts/addons_core/pose_library/operators.py b/scripts/addons_core/pose_library/operators.py index 9c6b791669c..57f125e6721 100644 --- a/scripts/addons_core/pose_library/operators.py +++ b/scripts/addons_core/pose_library/operators.py @@ -272,8 +272,8 @@ class POSELIB_OT_pose_asset_select_bones(PoseAssetUser, Operator): flipped: BoolProperty(name="Flipped", default=False) # type: ignore def use_pose(self, context: Context, pose_asset: Action) -> Set[str]: - arm_object: Object = context.object - pose_usage.select_bones(arm_object, pose_asset, select=self.select, flipped=self.flipped) + for object in context.selected_objects: + pose_usage.select_bones(object, pose_asset, select=self.select, flipped=self.flipped) if self.select: msg = tip_("Selected bones from %s") % pose_asset.name else: diff --git a/scripts/addons_core/pose_library/pose_usage.py b/scripts/addons_core/pose_library/pose_usage.py index 1a85f56ec55..a091fc63a3f 100644 --- a/scripts/addons_core/pose_library/pose_usage.py +++ b/scripts/addons_core/pose_library/pose_usage.py @@ -9,26 +9,59 @@ Pose Library - usage functions. from typing import Set import re import bpy +from bpy_extras import anim_utils from bpy.types import ( Action, Object, + ActionSlot, ) +def _find_best_slot(action: Action, object: Object) -> ActionSlot | None: + """ + Trying to find a slot that is the best match for the given object. + The best slot is either + the slot of the given object if that exists in the action, + or the first slot of type object + """ + if not action.slots: + return None + # For the selection code, the object doesn't need to be animated yet, so anim_data may be None. + anim_data = object.animation_data + + # last_slot_identifier will equal to the current slot identifier if one is assigned. + if anim_data and anim_data.last_slot_identifier in action.slots: + return action.slots[anim_data.last_slot_identifier] + + for slot in action.slots: + if slot.target_id_type == 'OBJECT': + return slot + return None + + def select_bones(arm_object: Object, action: Action, *, select: bool, flipped: bool) -> None: - pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') pose = arm_object.pose + if not pose: + return + + slot = _find_best_slot(action, arm_object) + if not slot: + return seen_bone_names: Set[str] = set() + channelbag = anim_utils.action_get_channelbag_for_slot(action, slot) + if not channelbag: + return - for fcurve in action.fcurves: + pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') + for fcurve in channelbag.fcurves: data_path: str = fcurve.data_path - match = pose_bone_re.match(data_path) - if not match: + regex_match = pose_bone_re.match(data_path) + if not regex_match: continue - bone_name = match.group(1) + bone_name = regex_match.group(1) if bone_name in seen_bone_names: continue diff --git a/scripts/modules/bpy_extras/anim_utils.py b/scripts/modules/bpy_extras/anim_utils.py index 20e537feaee..e566daad758 100644 --- a/scripts/modules/bpy_extras/anim_utils.py +++ b/scripts/modules/bpy_extras/anim_utils.py @@ -13,7 +13,7 @@ __all__ = ( ) import bpy -from bpy.types import Action, ActionSlot +from bpy.types import Action, ActionSlot, ActionChannelbag from dataclasses import dataclass from collections.abc import ( @@ -75,14 +75,18 @@ class BakeOptions: """Bake custom properties.""" -def _get_channelbag_for_slot(action: Action, slot: ActionSlot): - # This is on purpose limited to the first layer and strip. To support more - # than 1 layer, a rewrite of this operator is needed which ideally would - # happen in C++. +def action_get_channelbag_for_slot(action: Action, slot: ActionSlot) -> ActionChannelbag | None: + """ + Returns the first channelbag found for the slot. + In case there are multiple layers or strips they are iterated until a + channelbag for that slot is found. In case no matching channelbag is found, returns None. + """ for layer in action.layers: for strip in layer.strips: channelbag = strip.channelbag(slot) - return channelbag + if channelbag: + return channelbag + return None def _ensure_channelbag_exists(action: Action, slot: ActionSlot): @@ -409,7 +413,7 @@ def bake_action_iter( # pose lookup_fcurves = {} assert action.is_action_layered - channelbag = _get_channelbag_for_slot(action, atd.action_slot) + channelbag = action_get_channelbag_for_slot(action, atd.action_slot) if channelbag: # channelbag can be None if no layers or strips exist in the action. lookup_fcurves = {(fcurve.data_path, fcurve.array_index): fcurve for fcurve in channelbag.fcurves}