Christoph Lendenfeld 358a0479e8 Anim: create pose assets to different libraries
Similar to how brush assets are created and managed this
PR allows to export pose assets into a different library.
Because of this there is a limitation to this where each
asset is stored in a separate blend file.
This may be lifted in the future as there are planned changes in
the design phase: #122061

### Create Asset

Now available in the 3D viewport in the "Pose" menu: "Create Pose Asset".
The button in the Dope Sheet will now call this new operator as well.

Clicking either of those will open a popup in which you can:

* Choose the name of the asset, which library and catalog it goes into.
* Clicking "Create" will create a pose asset on disk in the given library.

It is possible to create files into an outside library or add it in the current file.
The latter option does a lot less since it basically just creates the
action and tags it as an asset.

If no Asset Shelf **AND** no Asset Browser is visible anywhere in Blender,
the Asset Shelf will be shown on the 3D viewport from which
the operator was called.

### Adjust Pose Asset

Right clicking a pose asset that has been created in the way described
before will have options to overwrite it.
Only the active object will be considered for updating a pose asset

Available Options (the latter 3 under the "Modify Pose Asset" submenu):
* Adjust Pose Asset: From the selected bones, update ONLY channels that
are also present in the asset. This is the default.
* Replace: Will completely replace the data in the Pose Asset from
the current selection
* Add: Adds the current selection to the Pose Asset. Any already existing
channels have their values updated
* Remove: Remove selected bones from the pose asset

Currently this refreshes the thumbnail. In the case of custom
thumbnails it might not be something want

### Deleting an existing Pose Asset

Right click on a Pose Asset and hit "Delete Pose Asset". Works in the shelf
and in the asset library. Doing so will pop up a confirmation dialog,
if confirming, the asset is gone forever. Deleting a local asset is basically the
same as clearing the asset. This is a bit confusing because you get
two options that basically do the same thing sometimes,
but "Delete" works in other cases as well.
I currently don't see a way around that.

Part of design #131840

Pull Request: https://projects.blender.org/blender/blender/pulls/132747
2025-02-04 11:29:05 +01:00

230 lines
7.1 KiB
Python

# SPDX-FileCopyrightText: 2021-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Pose Library - GUI definition.
"""
import bpy
from bpy.types import (
AssetRepresentation,
Context,
Menu,
Panel,
UILayout,
UIList,
)
from bl_ui_utils.layout import operator_context
class VIEW3D_MT_pose_modify(Menu):
bl_label = "Modify Pose Asset"
def draw(self, _context):
layout = self.layout
layout.operator("poselib.asset_modify", text="Replace").mode = "REPLACE"
layout.operator("poselib.asset_modify", text="Add Selected Bones").mode = "ADD"
layout.operator("poselib.asset_modify", text="Remove Selected Bones").mode = "REMOVE"
class PoseLibraryPanel:
@classmethod
def pose_library_panel_poll(cls, context: Context) -> bool:
return context.mode == 'POSE'
@classmethod
def poll(cls, context: Context) -> bool:
return cls.pose_library_panel_poll(context)
class VIEW3D_AST_pose_library(bpy.types.AssetShelf):
bl_space_type = "VIEW_3D"
# We have own keymap items to add custom drag behavior (pose blending), disable the default
# asset dragging.
bl_options = {'NO_ASSET_DRAG'}
@classmethod
def poll(cls, context: Context) -> bool:
return PoseLibraryPanel.poll(context)
@classmethod
def asset_poll(cls, asset: AssetRepresentation) -> bool:
return asset.id_type == 'ACTION'
@classmethod
def draw_context_menu(cls, _context: Context, _asset: AssetRepresentation, layout: UILayout):
layout.operator("poselib.apply_pose_asset", text="Apply Pose").flipped = False
layout.operator("poselib.apply_pose_asset", text="Apply Pose Flipped").flipped = True
with operator_context(layout, 'INVOKE_DEFAULT'):
layout.operator("poselib.blend_pose_asset", text="Blend Pose").flipped = False
layout.operator("poselib.blend_pose_asset", text="Blend Pose Flipped").flipped = True
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.asset_modify", text="Adjust Pose Asset").mode = 'ADJUST'
layout.menu("VIEW3D_MT_pose_modify")
layout.operator("poselib.asset_delete")
layout.separator()
layout.operator("asset.open_containing_blend_file")
def pose_library_asset_browser_context_menu(self: UIList, context: Context) -> None:
def is_pose_library_asset_browser() -> bool:
asset_library_ref = getattr(context, "asset_library_reference", None)
if not asset_library_ref:
return False
asset = getattr(context, "asset", None)
if not asset:
return False
return bool(asset.id_type == 'ACTION')
if not is_pose_library_asset_browser():
return
layout = self.layout
layout.separator()
layout.operator("poselib.apply_pose_asset", text="Apply Pose").flipped = False
layout.operator("poselib.apply_pose_asset", text="Apply Pose Flipped").flipped = True
with operator_context(layout, 'INVOKE_DEFAULT'):
layout.operator("poselib.blend_pose_asset", text="Blend Pose").flipped = False
layout.operator("poselib.blend_pose_asset", text="Blend Pose Flipped").flipped = True
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.asset_modify", text="Adjust Pose Asset").mode = 'ADJUST'
layout.menu("VIEW3D_MT_pose_modify")
with operator_context(layout, 'INVOKE_DEFAULT'):
layout.operator("poselib.asset_delete")
layout.separator()
layout.operator("asset.assign_action")
layout.separator()
class DOPESHEET_PT_asset_panel(PoseLibraryPanel, Panel):
bl_space_type = "DOPESHEET_EDITOR"
bl_region_type = "UI"
bl_label = "Create Pose Asset"
bl_category = "Action"
def draw(self, context: Context) -> None:
layout = self.layout
col = layout.column(align=True)
row = col.row(align=True)
row.operator("poselib.create_pose_asset")
if bpy.types.POSELIB_OT_restore_previous_action.poll(context):
row.operator("poselib.restore_previous_action", text="", icon='LOOP_BACK')
col.operator("poselib.copy_as_asset", icon="COPYDOWN")
layout.operator("poselib.convert_old_poselib")
def pose_library_list_item_asset_menu(self: UIList, context: Context) -> None:
layout = self.layout
layout.menu("ASSETBROWSER_MT_asset")
class ASSETBROWSER_MT_asset(Menu):
bl_label = "Asset"
@classmethod
def poll(cls, context):
from bpy_extras.asset_utils import SpaceAssetInfo
return SpaceAssetInfo.is_asset_browser_poll(context)
def draw(self, context: Context) -> None:
layout = self.layout
layout.operator("poselib.paste_asset", icon='PASTEDOWN')
layout.separator()
layout.operator("poselib.create_pose_asset")
# Messagebus subscription to monitor asset library changes.
_msgbus_owner = object()
def _on_asset_library_changed() -> None:
"""Update areas when a different asset library is selected."""
refresh_area_types = {'DOPESHEET_EDITOR', 'VIEW_3D'}
for win in bpy.context.window_manager.windows:
for area in win.screen.areas:
if area.type not in refresh_area_types:
continue
area.tag_redraw()
def register_message_bus() -> None:
bpy.msgbus.subscribe_rna(
key=(bpy.types.FileAssetSelectParams, "asset_library_reference"),
owner=_msgbus_owner,
args=(),
notify=_on_asset_library_changed,
options={'PERSISTENT'},
)
def unregister_message_bus() -> None:
bpy.msgbus.clear_by_owner(_msgbus_owner)
@bpy.app.handlers.persistent
def _on_blendfile_load_pre(none, other_none) -> None:
# The parameters are required, but both are None.
unregister_message_bus()
@bpy.app.handlers.persistent
def _on_blendfile_load_post(none, other_none) -> None:
# The parameters are required, but both are None.
register_message_bus()
classes = (
DOPESHEET_PT_asset_panel,
ASSETBROWSER_MT_asset,
VIEW3D_MT_pose_modify,
VIEW3D_AST_pose_library,
)
_register, _unregister = bpy.utils.register_classes_factory(classes)
def register() -> None:
_register()
bpy.types.ASSETBROWSER_MT_context_menu.prepend(pose_library_asset_browser_context_menu)
bpy.types.ASSETBROWSER_MT_editor_menus.append(pose_library_list_item_asset_menu)
register_message_bus()
bpy.app.handlers.load_pre.append(_on_blendfile_load_pre)
bpy.app.handlers.load_post.append(_on_blendfile_load_post)
def unregister() -> None:
_unregister()
unregister_message_bus()
bpy.types.ASSETBROWSER_MT_context_menu.remove(pose_library_asset_browser_context_menu)
bpy.types.ASSETBROWSER_MT_editor_menus.remove(pose_library_list_item_asset_menu)