From 9f8973074478c4bc7df0c59a96802b37e7b2b8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 5 Jun 2023 18:08:27 +0200 Subject: [PATCH 1/7] Animation: pose library, add 'Replace Pose' operator Add a 'Replace Pose' operator that completely replaces the selected pose asset. It creates a new pose asset just like 'Create Pose', copies the asset metadata from the selected asset to the newly created one, then removes the selected asset. This requires https://projects.blender.org/blender/blender/pulls/108547 --- pose_library/gui.py | 4 ++- pose_library/operators.py | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/pose_library/gui.py b/pose_library/gui.py index 8f0360780..482e1a08f 100644 --- a/pose_library/gui.py +++ b/pose_library/gui.py @@ -90,6 +90,8 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None: with operator_context(layout, 'INVOKE_DEFAULT'): layout.operator("poselib.blend_pose_asset", text="Blend Pose") + layout.operator("poselib.replace_pose_asset", text="Replace Pose") + layout.separator() props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones") props.select = True @@ -147,7 +149,7 @@ class ASSETBROWSER_MT_asset(Menu): layout.operator("poselib.create_pose_asset").activate_new_action = False -### Messagebus subscription to monitor asset library changes. +# Messagebus subscription to monitor asset library changes. _msgbus_owner = object() diff --git a/pose_library/operators.py b/pose_library/operators.py index c87c74db1..145619d26 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -139,6 +139,71 @@ class POSELIB_OT_create_pose_asset(PoseAssetCreator, Operator): self.report({'WARNING'}, tip_("Action %s marked Fake User to prevent loss") % action.name) +class POSELIB_OT_replace_pose_asset(Operator): + bl_idname = "poselib.replace_pose_asset" + bl_label = "Replace Pose Asset" + bl_description = ( + "Create a new Action that contains the pose of the selected bones, and use that to replace the selected Asset" + ) + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + if not isinstance(getattr(context, "id", None), Action): + return False + + if context.object is None or context.object.mode != "POSE": + # The operator assumes pose mode, so that bone selection is visible. + cls.poll_message_set("An active armature object in pose mode is needed") + return False + + asset_space_params = asset_browser.params(context.area) + if asset_space_params.asset_library_ref != 'LOCAL': + cls.poll_message_set("Asset Browser must be set to the Current File library") + return False + + return True + + def execute(self, context: Context) -> Set[str]: + pose_name = context.id.name # This will not use this name entirely, but it'll be made unique. + asset = pose_creation.create_pose_asset_from_context(context, pose_name) + if not asset: + self.report({"WARNING"}, "No keyframes were found for this pose") + return {"CANCELLED"} + + # Copy the asset metadata to the newly created asset. + asset.asset_data = context.id.asset_data + + self._activate_asset_in_browser(context, asset) + + # Delete the old asset + bpy.data.actions.remove(context.id) + # Now that the old name has become available, rename the asset to it. + asset.name = pose_name + + return {'FINISHED'} + + def _activate_asset_in_browser(self, context: Context, asset: Action) -> None: + """Activate the new asset in the appropriate Asset Browser. + + This makes it possible to immediately check & edit the created pose asset. + """ + + asset_browse_area: Optional[bpy.types.Area] = asset_browser.area_from_context(context) + if not asset_browse_area: + return + + # After creating an asset, the window manager has to process the + # notifiers before editors should be manipulated. + pose_creation.assign_from_asset_browser(asset, asset_browse_area) + + # Pass deferred=True, because we just created a new asset that isn't + # known to the Asset Browser space yet. That requires the processing of + # notifiers, which will only happen after this code has finished + # running. + asset_browser.activate_asset(asset, asset_browse_area, deferred=True) + + class POSELIB_OT_restore_previous_action(Operator): bl_idname = "poselib.restore_previous_action" bl_label = "Restore Previous Action" @@ -450,6 +515,7 @@ classes = ( POSELIB_OT_convert_old_object_poselib, POSELIB_OT_copy_as_asset, POSELIB_OT_create_pose_asset, + POSELIB_OT_replace_pose_asset, POSELIB_OT_paste_asset, POSELIB_OT_pose_asset_select_bones, POSELIB_OT_restore_previous_action, -- 2.30.2 From f1e7ff64ddf57594dab8a2fb79cf50da7c9e5284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 6 Jun 2023 13:07:10 +0200 Subject: [PATCH 2/7] Add 'Update Pose' operator --- pose_library/gui.py | 1 + pose_library/operators.py | 135 +++++++++++++++++++++++++++++++++- pose_library/pose_creation.py | 47 ++++++++---- 3 files changed, 166 insertions(+), 17 deletions(-) diff --git a/pose_library/gui.py b/pose_library/gui.py index 482e1a08f..d2b42e245 100644 --- a/pose_library/gui.py +++ b/pose_library/gui.py @@ -91,6 +91,7 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None: layout.operator("poselib.blend_pose_asset", text="Blend Pose") layout.operator("poselib.replace_pose_asset", text="Replace Pose") + layout.operator("poselib.update_pose_asset", text="Update Pose") layout.separator() props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones") diff --git a/pose_library/operators.py b/pose_library/operators.py index 145619d26..f231b2bcf 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -24,7 +24,7 @@ from bpy.props import BoolProperty, StringProperty from bpy.types import ( Action, Context, - Event, + FCurve, FileSelectEntry, Object, Operator, @@ -204,6 +204,138 @@ class POSELIB_OT_replace_pose_asset(Operator): asset_browser.activate_asset(asset, asset_browse_area, deferred=True) +class POSELIB_OT_update_pose_asset(Operator): + bl_idname = "poselib.update_pose_asset" + bl_label = "Update Pose Asset" + bl_description = ( + "Update the selected pose asset with the selected bones, leaving the unselected bone channels as-is" + ) + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + if not isinstance(getattr(context, "id", None), Action): + return False + + if context.object is None or context.object.mode != "POSE": + # The operator assumes pose mode, so that bone selection is visible. + cls.poll_message_set("An active armature object in pose mode is needed") + return False + + asset_space_params = asset_browser.params(context.area) + if asset_space_params.asset_library_ref != 'LOCAL': + cls.poll_message_set("Asset Browser must be set to the Current File library") + return False + + return True + + def execute(self, context: Context) -> Set[str]: + # Make sure the new keys are on the same frame as the old keys. + storage_frame_nr = self._find_frame_number(context) + + params = pose_creation.params_for_selected_bones( + context, + "temporary-pose-action", + dest_frame_nr=storage_frame_nr, + ) + + # Create a temporary pose Action. This will be used to update the existing pose asset. + creator = pose_creation.PoseActionCreator(params) + new_pose = creator.create() + if new_pose is None: + self.report({"WARNING"}, "Select the to-be-updated bones") + return {"CANCELLED"} + + try: + original_pose: Action = context.id + was_updated = self._update_pose_asset(original_pose, new_pose, storage_frame_nr) + finally: + # Clean up by removing the temporary pose Action. + bpy.data.actions.remove(new_pose) + + return {'FINISHED'} if was_updated else {'CANCELLED'} + + def _update_pose_asset(self, + original_pose: Action, + new_pose: Action, + storage_frame_nr: float,) -> bool: + """Perform the pose update. + + :return: True if the original pose was altered, False otherwise. + """ + + # Create lookup table for FCurves. There's also Action.fcurves.find() but + # that would do a linear search through all FCurves in the action, for + # each lookup. + original_fcurve_lookup: dict[tuple[str, int], FCurve] = { + (fcu.data_path, fcu.array_index): fcu + for fcu in original_pose.fcurves + } + + num_fcurves_added = 0 + num_fcurves_edited = 0 + for fcu in new_pose.fcurves: + fcu_value = fcu.keyframe_points[0].co.y + try: + orig_fcu = original_fcurve_lookup[fcu.data_path, fcu.array_index] + except KeyError: + orig_fcu = None + + if orig_fcu is None: + # No such FCurve, so create a new one. + group_name = getattr(fcu.group, 'name', '') + new_fcu = original_pose.fcurves.new( + fcu.data_path, + index=fcu.array_index, + action_group=group_name, + ) + new_fcu.keyframe_points.insert(storage_frame_nr, value=fcu_value) + new_fcu.update() + + num_fcurves_added += 1 + continue + + # Update the exsting FCurve. This assumes that it is a valid + # pose asset, i.e. that it has a single key. + kp = orig_fcu.keyframe_points[0] + old_value = kp.co.y + + # Don't bother updating the FCurve if the value was unchanged. + if abs(old_value - fcu_value) < 0.0001: + continue + + # Use co_ui to move the handles along with the value. + kp.co_ui.y = fcu_value + orig_fcu.update() + + num_fcurves_edited += 1 + + if num_fcurves_added == 0 and num_fcurves_edited == 0: + self.report({'WARNING'}, "Pose did not change, so the asset was not updated") + return False + + original_pose.asset_generate_preview() + + # Present the results. + msg_parts = [] + if num_fcurves_added: + msg_parts.append("%d FCurves added" % num_fcurves_added) + if num_fcurves_edited: + msg_parts.append("%d FCurves edited" % num_fcurves_edited) + msg = ", ".join(msg_parts) + self.report({'INFO'}, "Updated pose: %s" % msg) + + return True + + @staticmethod + def _find_frame_number(context: Context) -> float: + original_pose: Action = context.id + assert isinstance(original_pose, Action), f"expected an Action, got {original_pose}" + if original_pose.fcurves and original_pose.fcurves[0].keyframe_points: + return original_pose.fcurves[0].keyframe_points[0].co.x + return context.scene.frame_current + + class POSELIB_OT_restore_previous_action(Operator): bl_idname = "poselib.restore_previous_action" bl_label = "Restore Previous Action" @@ -516,6 +648,7 @@ classes = ( POSELIB_OT_copy_as_asset, POSELIB_OT_create_pose_asset, POSELIB_OT_replace_pose_asset, + POSELIB_OT_update_pose_asset, POSELIB_OT_paste_asset, POSELIB_OT_pose_asset_select_bones, POSELIB_OT_restore_previous_action, diff --git a/pose_library/pose_creation.py b/pose_library/pose_creation.py index e1faf39cc..1fe3cd940 100644 --- a/pose_library/pose_creation.py +++ b/pose_library/pose_creation.py @@ -36,8 +36,9 @@ pose_bone_re = re.compile(r'pose.bones\["([^"]+)"\]') @dataclasses.dataclass(unsafe_hash=True, frozen=True) class PoseCreationParams: armature_ob: bpy.types.Object - src_action: Optional[Action] + src_action: Optional[Action] # For copying non-transform properties when they have been keyed. src_frame_nr: float + dest_frame_nr: float bone_names: FrozenSet[str] new_asset_name: str @@ -130,7 +131,7 @@ class PoseActionCreator: continue # Only include in the pose if there is a key on this frame. - if not self._has_key_on_frame(fcurve): + if not self._has_key_on_frame(fcurve, self.params.src_frame_nr): continue try: @@ -140,7 +141,7 @@ class PoseActionCreator: continue dst_fcurve = dst_action.fcurves.new(fcurve.data_path, index=fcurve.array_index, action_group=bone_name) - dst_fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value) + dst_fcurve.keyframe_points.insert(self.params.dest_frame_nr, value=value) dst_fcurve.update() def _store_location(self, dst_action: Action, bone_name: str) -> None: @@ -193,7 +194,7 @@ class PoseActionCreator: if fcurve is None: fcurve = dst_action.fcurves.new(rna_path, index=array_index, action_group=bone_name) - fcurve.keyframe_points.insert(self.params.src_frame_nr, value=value) + fcurve.keyframe_points.insert(self.params.dest_frame_nr, value=value) fcurve.update() @classmethod @@ -244,20 +245,20 @@ class PoseActionCreator: bone: Bone = self.params.armature_ob.pose.bones[bone_name] return bone - def _has_key_on_frame(self, fcurve: FCurve) -> bool: - """Return True iff the FCurve has a key on the source frame.""" + @staticmethod + def _has_key_on_frame(fcurve: FCurve, frame_nr: float) -> bool: + """Return True iff the FCurve has a key on the given frame.""" points = fcurve.keyframe_points if not points: return False - frame_to_find = self.params.src_frame_nr margin = 0.001 high = len(points) - 1 low = 0 while low <= high: mid = (high + low) // 2 - diff = points[mid].co.x - frame_to_find + diff = points[mid].co.x - frame_nr if abs(diff) < margin: return True if diff < 0: @@ -290,18 +291,32 @@ def create_pose_asset( def create_pose_asset_from_context(context: Context, new_asset_name: str) -> Optional[Action]: """Create Action asset from active object & selected bones.""" + params = params_for_selected_bones(context, new_asset_name) + return create_pose_asset(params) + + +def params_for_selected_bones( + context: Context, + new_asset_name: str, + dest_frame_nr: float | None = None, +) -> PoseCreationParams: + """Return suitable pose asset creation parameters for the selected pose bones.""" + bones = context.selected_pose_bones_from_active_object bone_names = {bone.name for bone in bones} - params = PoseCreationParams( - context.object, - getattr(context.object.animation_data, "action", None), - context.scene.frame_current, - frozenset(bone_names), - new_asset_name, - ) + if dest_frame_nr is None: + dest_frame_nr = context.scene.frame_current - return create_pose_asset(params) + params = PoseCreationParams( + armature_ob=context.object, + src_action=getattr(context.object.animation_data, "action", None), + src_frame_nr=context.scene.frame_current, + dest_frame_nr=dest_frame_nr, + bone_names=frozenset(bone_names), + new_asset_name=new_asset_name, + ) + return params def copy_fcurves( -- 2.30.2 From 19a210568fcadd3a6d5249395cbf341774f41de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 6 Jun 2023 14:47:34 +0200 Subject: [PATCH 3/7] Refactor, no functional changes --- pose_library/gui.py | 7 ++-- pose_library/operators.py | 79 ++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/pose_library/gui.py b/pose_library/gui.py index d2b42e245..4a0041687 100644 --- a/pose_library/gui.py +++ b/pose_library/gui.py @@ -90,15 +90,16 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None: with operator_context(layout, 'INVOKE_DEFAULT'): layout.operator("poselib.blend_pose_asset", text="Blend Pose") - layout.operator("poselib.replace_pose_asset", text="Replace Pose") - layout.operator("poselib.update_pose_asset", text="Update Pose") - layout.separator() props = layout.operator("poselib.pose_asset_select_bones", text="Select Pose Bones") props.select = True props = layout.operator("poselib.pose_asset_select_bones", text="Deselect Pose Bones") props.select = False + layout.separator() + layout.operator("poselib.replace_pose_asset", text="Replace Pose Asset") + layout.operator("poselib.update_pose_asset", text="Update Pose Asset") + if not is_pose_asset_view(): layout.separator() layout.operator("asset.assign_action") diff --git a/pose_library/operators.py b/pose_library/operators.py index f231b2bcf..f36d29537 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -248,7 +248,7 @@ class POSELIB_OT_update_pose_asset(Operator): try: original_pose: Action = context.id - was_updated = self._update_pose_asset(original_pose, new_pose, storage_frame_nr) + was_updated = self._update_pose_asset(original_pose, new_pose) finally: # Clean up by removing the temporary pose Action. bpy.data.actions.remove(new_pose) @@ -256,12 +256,11 @@ class POSELIB_OT_update_pose_asset(Operator): return {'FINISHED'} if was_updated else {'CANCELLED'} def _update_pose_asset(self, - original_pose: Action, - new_pose: Action, - storage_frame_nr: float,) -> bool: - """Perform the pose update. + pose_to_update: Action, + update_source: Action) -> bool: + """Copy keys from `update_source` to `pose_to_update`. - :return: True if the original pose was altered, False otherwise. + :return: True if `pose_to_update` was altered, False otherwise. """ # Create lookup table for FCurves. There's also Action.fcurves.find() but @@ -269,52 +268,31 @@ class POSELIB_OT_update_pose_asset(Operator): # each lookup. original_fcurve_lookup: dict[tuple[str, int], FCurve] = { (fcu.data_path, fcu.array_index): fcu - for fcu in original_pose.fcurves + for fcu in pose_to_update.fcurves } num_fcurves_added = 0 num_fcurves_edited = 0 - for fcu in new_pose.fcurves: - fcu_value = fcu.keyframe_points[0].co.y + for fcu in update_source.fcurves: try: orig_fcu = original_fcurve_lookup[fcu.data_path, fcu.array_index] except KeyError: orig_fcu = None if orig_fcu is None: - # No such FCurve, so create a new one. - group_name = getattr(fcu.group, 'name', '') - new_fcu = original_pose.fcurves.new( - fcu.data_path, - index=fcu.array_index, - action_group=group_name, - ) - new_fcu.keyframe_points.insert(storage_frame_nr, value=fcu_value) - new_fcu.update() - + self._create_new_fcurve(pose_to_update, fcu) num_fcurves_added += 1 continue - # Update the exsting FCurve. This assumes that it is a valid - # pose asset, i.e. that it has a single key. - kp = orig_fcu.keyframe_points[0] - old_value = kp.co.y + was_updated = self._update_existing_fcurve(pose_to_update, orig_fcu, fcu) + if was_updated: + num_fcurves_edited += 1 - # Don't bother updating the FCurve if the value was unchanged. - if abs(old_value - fcu_value) < 0.0001: - continue - - # Use co_ui to move the handles along with the value. - kp.co_ui.y = fcu_value - orig_fcu.update() - - num_fcurves_edited += 1 - - if num_fcurves_added == 0 and num_fcurves_edited == 0: + if not any((num_fcurves_added, num_fcurves_edited)): self.report({'WARNING'}, "Pose did not change, so the asset was not updated") return False - original_pose.asset_generate_preview() + pose_to_update.asset_generate_preview() # Present the results. msg_parts = [] @@ -335,6 +313,37 @@ class POSELIB_OT_update_pose_asset(Operator): return original_pose.fcurves[0].keyframe_points[0].co.x return context.scene.frame_current + @staticmethod + def _create_new_fcurve(action: Action, fcu_to_copy: FCurve) -> None: + group_name = getattr(fcu_to_copy.group, 'name', '') + + new_fcu = action.fcurves.new( + fcu_to_copy.data_path, + index=fcu_to_copy.array_index, + action_group=group_name, + ) + + keyframe_co = fcu_to_copy.keyframe_points[0].co + new_fcu.keyframe_points.insert(keyframe_co.x, value=keyframe_co.y) + new_fcu.update() + + @staticmethod + def _update_existing_fcurve(action: Action, fcu_existing: FCurve, fcu_to_read: FCurve) -> bool: + # Update the exsting FCurve. This assumes that it is a valid + # pose asset, i.e. that the FCurve has a single key. + kp = fcu_existing.keyframe_points[0] + old_value = kp.co.y + + # Don't bother updating the FCurve if the value was unchanged. + fcu_value = fcu_to_read.keyframe_points[0].co.y + if abs(old_value - fcu_value) < 0.0001: + return False + + # Use co_ui to move the handles along with the value. + kp.co_ui.y = fcu_value + fcu_existing.update() + return True + class POSELIB_OT_restore_previous_action(Operator): bl_idname = "poselib.restore_previous_action" -- 2.30.2 From cc239740a7983a25bd5f4041bb64a5d01bf7e1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 6 Jun 2023 14:56:15 +0200 Subject: [PATCH 4/7] Swap replace & update in the context menu --- pose_library/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pose_library/gui.py b/pose_library/gui.py index 4a0041687..0f9fc32c4 100644 --- a/pose_library/gui.py +++ b/pose_library/gui.py @@ -97,8 +97,8 @@ def pose_library_list_item_context_menu(self: UIList, context: Context) -> None: props.select = False layout.separator() - layout.operator("poselib.replace_pose_asset", text="Replace Pose Asset") layout.operator("poselib.update_pose_asset", text="Update Pose Asset") + layout.operator("poselib.replace_pose_asset", text="Replace Pose Asset") if not is_pose_asset_view(): layout.separator() -- 2.30.2 From a9057116a12bf276596cbffbb260ff337f10fae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 6 Jun 2023 15:09:54 +0200 Subject: [PATCH 5/7] Make the operators compatible with the asset view as well --- pose_library/operators.py | 41 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/pose_library/operators.py b/pose_library/operators.py index f36d29537..ca3d3a36f 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -139,6 +139,20 @@ class POSELIB_OT_create_pose_asset(PoseAssetCreator, Operator): self.report({'WARNING'}, tip_("Action %s marked Fake User to prevent loss") % action.name) +def _get_action(context: Context) -> Action | None: + """Get the current action from the asset browser or the asset view.""" + + if context.asset_library_ref and context.asset_file_handle and context.asset_file_handle.id_type == 'ACTION': + context_id = context.asset_file_handle.local_id + else: + context_id = getattr(context, "id", None) + + if not isinstance(context_id, Action): + return None + + return context_id + + class POSELIB_OT_replace_pose_asset(Operator): bl_idname = "poselib.replace_pose_asset" bl_label = "Replace Pose Asset" @@ -149,7 +163,7 @@ class POSELIB_OT_replace_pose_asset(Operator): @classmethod def poll(cls, context: Context) -> bool: - if not isinstance(getattr(context, "id", None), Action): + if not _get_action(context): return False if context.object is None or context.object.mode != "POSE": @@ -165,19 +179,21 @@ class POSELIB_OT_replace_pose_asset(Operator): return True def execute(self, context: Context) -> Set[str]: - pose_name = context.id.name # This will not use this name entirely, but it'll be made unique. + original_pose: Action = _get_action(context) + + pose_name = original_pose.name # This will not use this name entirely, but it'll be made unique. asset = pose_creation.create_pose_asset_from_context(context, pose_name) if not asset: self.report({"WARNING"}, "No keyframes were found for this pose") return {"CANCELLED"} # Copy the asset metadata to the newly created asset. - asset.asset_data = context.id.asset_data + asset.asset_data = original_pose.asset_data self._activate_asset_in_browser(context, asset) # Delete the old asset - bpy.data.actions.remove(context.id) + bpy.data.actions.remove(original_pose) # Now that the old name has become available, rename the asset to it. asset.name = pose_name @@ -214,7 +230,7 @@ class POSELIB_OT_update_pose_asset(Operator): @classmethod def poll(cls, context: Context) -> bool: - if not isinstance(getattr(context, "id", None), Action): + if not _get_action(context): return False if context.object is None or context.object.mode != "POSE": @@ -231,7 +247,8 @@ class POSELIB_OT_update_pose_asset(Operator): def execute(self, context: Context) -> Set[str]: # Make sure the new keys are on the same frame as the old keys. - storage_frame_nr = self._find_frame_number(context) + original_pose: Action = _get_action(context) + storage_frame_nr = self._find_frame_number(original_pose, context.scene.frame_current) params = pose_creation.params_for_selected_bones( context, @@ -247,7 +264,6 @@ class POSELIB_OT_update_pose_asset(Operator): return {"CANCELLED"} try: - original_pose: Action = context.id was_updated = self._update_pose_asset(original_pose, new_pose) finally: # Clean up by removing the temporary pose Action. @@ -306,12 +322,11 @@ class POSELIB_OT_update_pose_asset(Operator): return True @staticmethod - def _find_frame_number(context: Context) -> float: - original_pose: Action = context.id - assert isinstance(original_pose, Action), f"expected an Action, got {original_pose}" - if original_pose.fcurves and original_pose.fcurves[0].keyframe_points: - return original_pose.fcurves[0].keyframe_points[0].co.x - return context.scene.frame_current + def _find_frame_number(action: Action, fallback_frame: float) -> float: + assert isinstance(action, Action), f"expected an Action, got {action}" + if action.fcurves and action.fcurves[0].keyframe_points: + return action.fcurves[0].keyframe_points[0].co.x + return fallback_frame @staticmethod def _create_new_fcurve(action: Action, fcu_to_copy: FCurve) -> None: -- 2.30.2 From f16731a4ca17c13fc1607ea7cdd94fd18a6491c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 6 Jun 2023 17:39:37 +0200 Subject: [PATCH 6/7] Remove assumption from poll functions that it's used in the asset browser --- pose_library/operators.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/pose_library/operators.py b/pose_library/operators.py index e510ead68..2cf534b27 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -164,6 +164,7 @@ class POSELIB_OT_replace_pose_asset(Operator): @classmethod def poll(cls, context: Context) -> bool: if not _get_action(context): + cls.poll_message_set("Select a pose asset from the currently open blend file") return False if context.object is None or context.object.mode != "POSE": @@ -171,11 +172,6 @@ class POSELIB_OT_replace_pose_asset(Operator): cls.poll_message_set("An active armature object in pose mode is needed") return False - asset_space_params = asset_browser.params(context.area) - if asset_space_params.asset_library_ref != 'LOCAL': - cls.poll_message_set("Asset Browser must be set to the Current File library") - return False - return True def execute(self, context: Context) -> Set[str]: @@ -230,20 +226,7 @@ class POSELIB_OT_update_pose_asset(Operator): @classmethod def poll(cls, context: Context) -> bool: - if not _get_action(context): - return False - - if context.object is None or context.object.mode != "POSE": - # The operator assumes pose mode, so that bone selection is visible. - cls.poll_message_set("An active armature object in pose mode is needed") - return False - - asset_space_params = asset_browser.params(context.area) - if asset_space_params.asset_library_ref != 'LOCAL': - cls.poll_message_set("Asset Browser must be set to the Current File library") - return False - - return True + return POSELIB_OT_replace_pose_asset(context) def execute(self, context: Context) -> Set[str]: # Make sure the new keys are on the same frame as the old keys. -- 2.30.2 From 59ca7966637251a78bc1e60652fa0e84d58b0e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 8 Jun 2023 15:24:09 +0200 Subject: [PATCH 7/7] Feedback from Nathan --- pose_library/operators.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pose_library/operators.py b/pose_library/operators.py index 2cf534b27..303b9327f 100644 --- a/pose_library/operators.py +++ b/pose_library/operators.py @@ -329,16 +329,15 @@ class POSELIB_OT_update_pose_asset(Operator): def _update_existing_fcurve(action: Action, fcu_existing: FCurve, fcu_to_read: FCurve) -> bool: # Update the exsting FCurve. This assumes that it is a valid # pose asset, i.e. that the FCurve has a single key. - kp = fcu_existing.keyframe_points[0] - old_value = kp.co.y + keyframe = fcu_existing.keyframe_points[0] # Don't bother updating the FCurve if the value was unchanged. fcu_value = fcu_to_read.keyframe_points[0].co.y - if abs(old_value - fcu_value) < 0.0001: + if keyframe.co.y == fcu_value: return False # Use co_ui to move the handles along with the value. - kp.co_ui.y = fcu_value + keyframe.co_ui.y = fcu_value fcu_existing.update() return True -- 2.30.2