Refactor: convert "Copy Pose Asset" operator to use the current Action API

Convert the Copy Pose Asset code from the legacy Action API to the
current API (introduced in Blender 4.4).

No functional changes.

This is part of #146586

Pull Request: https://projects.blender.org/blender/blender/pulls/147060
This commit is contained in:
Sybren A. Stüvel
2025-09-23 15:47:04 +02:00
parent 0b978a20e7
commit c77ac91ee4

View File

@@ -23,11 +23,14 @@ else:
import bpy
from bpy.types import (
Action,
ActionChannelbag,
ActionSlot,
Bone,
Context,
FCurve,
Keyframe,
)
from bpy_extras import anim_utils
FCurveValue = Union[float, int]
@@ -39,6 +42,7 @@ pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]')
class PoseCreationParams:
armature_ob: bpy.types.Object
src_action: Optional[Action]
src_action_slot: Optional[ActionSlot]
src_frame_nr: float
bone_names: FrozenSet[str]
new_asset_name: str
@@ -77,13 +81,15 @@ class PoseActionCreator:
try:
dst_action = self._create_new_action()
self._store_pose(dst_action)
slot = dst_action.slots.new(self.params.armature_ob.id_type, self.params.armature_ob.name)
channelbag = anim_utils.action_ensure_channelbag_for_slot(dst_action, slot)
self._store_pose(channelbag)
finally:
# Prevent next instantiations of this class from reusing pointers to
# bones. They may not be valid by then any more.
self._find_bone.cache_clear()
if len(dst_action.fcurves) == 0:
if len(channelbag.fcurves) == 0:
bpy.data.actions.remove(dst_action)
return None
@@ -94,27 +100,30 @@ class PoseActionCreator:
dst_action.user_clear() # actions.new() sets users=1, but marking as asset also increments user count.
return dst_action
def _store_pose(self, dst_action: Action) -> None:
def _store_pose(self, dst_channelbag: ActionChannelbag) -> None:
"""Store the current pose into the given action."""
self._store_bone_pose_parameters(dst_action)
self._store_animated_parameters(dst_action)
self._store_bone_pose_parameters(dst_channelbag)
self._store_animated_parameters(dst_channelbag)
def _store_bone_pose_parameters(self, dst_action: Action) -> None:
def _store_bone_pose_parameters(self, dst_channelbag: ActionChannelbag) -> None:
"""Store loc/rot/scale/bbone values in the Action."""
for bone_name in sorted(self.params.bone_names):
self._store_location(dst_action, bone_name)
self._store_rotation(dst_action, bone_name)
self._store_scale(dst_action, bone_name)
self._store_bbone(dst_action, bone_name)
self._store_location(dst_channelbag, bone_name)
self._store_rotation(dst_channelbag, bone_name)
self._store_scale(dst_channelbag, bone_name)
self._store_bbone(dst_channelbag, bone_name)
def _store_animated_parameters(self, dst_action: Action) -> None:
def _store_animated_parameters(self, dst_channelbag: ActionChannelbag) -> None:
"""Store the current value of any animated bone properties."""
if self.params.src_action is None:
return
src_channelbag = anim_utils.action_get_channelbag_for_slot(self.params.src_action, self.params.src_action_slot)
if not src_channelbag:
return
armature_ob = self.params.armature_ob
for fcurve in self.params.src_action.fcurves:
for fcurve in src_channelbag.fcurves:
match = pose_bone_re.match(fcurve.data_path)
if not match:
# Not animating a bone property.
@@ -125,7 +134,7 @@ class PoseActionCreator:
# Bone is not our export set.
continue
if dst_action.fcurves.find(fcurve.data_path, index=fcurve.array_index):
if dst_channelbag.fcurves.find(fcurve.data_path, index=fcurve.array_index):
# This property is already handled by a previous _store_xxx() call.
continue
@@ -139,44 +148,49 @@ class PoseActionCreator:
# A once-animated property no longer exists.
continue
dst_fcurve = dst_action.fcurves.new(fcurve.data_path, index=fcurve.array_index, action_group=bone_name)
dst_fcurve = dst_channelbag.fcurves.new(fcurve.data_path, index=fcurve.array_index, group_name=bone_name)
dst_fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value)
dst_fcurve.update()
def _store_location(self, dst_action: Action, bone_name: str) -> None:
def _store_location(self, dst_channelbag: ActionChannelbag, bone_name: str) -> None:
"""Store bone location."""
self._store_bone_array(dst_action, bone_name, "location", 3)
self._store_bone_array(dst_channelbag, bone_name, "location", 3)
def _store_rotation(self, dst_action: Action, bone_name: str) -> None:
def _store_rotation(self, dst_channelbag: ActionChannelbag, bone_name: str) -> None:
"""Store bone rotation given current rotation mode."""
bone = self._find_bone(bone_name)
if bone.rotation_mode == "QUATERNION":
self._store_bone_array(dst_action, bone_name, "rotation_quaternion", 4)
self._store_bone_array(dst_channelbag, bone_name, "rotation_quaternion", 4)
elif bone.rotation_mode == "AXIS_ANGLE":
self._store_bone_array(dst_action, bone_name, "rotation_axis_angle", 4)
self._store_bone_array(dst_channelbag, bone_name, "rotation_axis_angle", 4)
else:
self._store_bone_array(dst_action, bone_name, "rotation_euler", 3)
self._store_bone_array(dst_channelbag, bone_name, "rotation_euler", 3)
def _store_scale(self, dst_action: Action, bone_name: str) -> None:
def _store_scale(self, dst_channelbag: ActionChannelbag, bone_name: str) -> None:
"""Store bone scale."""
self._store_bone_array(dst_action, bone_name, "scale", 3)
self._store_bone_array(dst_channelbag, bone_name, "scale", 3)
def _store_bbone(self, dst_action: Action, bone_name: str) -> None:
def _store_bbone(self, dst_channelbag: ActionChannelbag, bone_name: str) -> None:
"""Store bendy-bone parameters."""
for prop_name, array_length in self._bbone_props:
if array_length:
self._store_bone_array(dst_action, bone_name, prop_name, array_length)
self._store_bone_array(dst_channelbag, bone_name, prop_name, array_length)
else:
self._store_bone_property(dst_action, bone_name, prop_name)
self._store_bone_property(dst_channelbag, bone_name, prop_name)
def _store_bone_array(self, dst_action: Action, bone_name: str, property_name: str, array_length: int) -> None:
def _store_bone_array(
self,
dst_channelbag: ActionChannelbag,
bone_name: str,
property_name: str,
array_length: int) -> None:
"""Store all elements of an array property."""
for array_index in range(array_length):
self._store_bone_property(dst_action, bone_name, property_name, array_index)
self._store_bone_property(dst_channelbag, bone_name, property_name, array_index)
def _store_bone_property(
self,
dst_action: Action,
dst_channelbag: ActionChannelbag,
bone_name: str,
property_path: str,
array_index: int = -1,
@@ -189,9 +203,10 @@ class PoseActionCreator:
# Get the full 'pose.bones["bone_name"].blablabla' path suitable for FCurves.
rna_path = bone.path_from_id(property_path)
fcurve: Optional[FCurve] = dst_action.fcurves.find(rna_path, index=array_index)
fcurve: Optional[FCurve] = dst_channelbag.fcurves.find(rna_path, index=array_index)
if fcurve is None:
fcurve = dst_action.fcurves.new(rna_path, index=array_index, action_group=bone_name)
fcurve = dst_channelbag.fcurves.new(rna_path, index=array_index, group_name=bone_name)
assert fcurve is not None
fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value)
fcurve.update()
@@ -305,6 +320,7 @@ def create_pose_asset_from_context(context: Context, new_asset_name: str) -> Opt
params = PoseCreationParams(
context.object,
getattr(context.object.animation_data, "action", None),
getattr(context.object.animation_data, "action_slot", None),
context.scene.frame_current,
frozenset(bone_names),
new_asset_name,
@@ -313,22 +329,25 @@ def create_pose_asset_from_context(context: Context, new_asset_name: str) -> Opt
return create_pose_asset(params)
def create_single_key_fcurve(dst_action: Action, src_fcurve: FCurve, src_keyframe: Keyframe) -> FCurve:
def create_single_key_fcurve(dst_channelbag: ActionChannelbag, src_fcurve: FCurve, src_keyframe: Keyframe) -> FCurve:
"""Create a copy of the source FCurve, but only for the given keyframe.
Returns a new FCurve with just one keyframe.
"""
dst_fcurve = copy_fcurve_without_keys(dst_action, src_fcurve)
dst_fcurve = copy_fcurve_without_keys(dst_channelbag, src_fcurve)
copy_keyframe(dst_fcurve, src_keyframe)
return dst_fcurve
def copy_fcurve_without_keys(dst_action: Action, src_fcurve: FCurve) -> FCurve:
def copy_fcurve_without_keys(dst_channelbag: ActionChannelbag, src_fcurve: FCurve) -> FCurve:
"""Create a new FCurve and copy some properties."""
src_group_name = src_fcurve.group.name if src_fcurve.group else ""
dst_fcurve = dst_action.fcurves.new(src_fcurve.data_path, index=src_fcurve.array_index, action_group=src_group_name)
dst_fcurve = dst_channelbag.fcurves.new(
src_fcurve.data_path,
index=src_fcurve.array_index,
group_name=src_group_name)
for propname in {"auto_smoothing", "color", "color_mode", "extrapolation"}:
setattr(dst_fcurve, propname, getattr(src_fcurve, propname))
return dst_fcurve