# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### """ Pose Library - operators. """ from pathlib import Path from typing import Optional, Set _need_reload = "functions" in locals() from . import asset_browser, functions, pose_creation, pose_usage if _need_reload: import importlib asset_browser = importlib.reload(asset_browser) functions = importlib.reload(functions) pose_creation = importlib.reload(pose_creation) pose_usage = importlib.reload(pose_usage) import bpy from bpy.props import BoolProperty, StringProperty from bpy.types import ( Action, Context, Event, FileSelectEntry, Object, Operator, ) class PoseAssetCreator: @classmethod def poll(cls, context: Context) -> bool: return bool( # There must be an object. context.object # It must be in pose mode with selected bones. and context.object.mode == "POSE" and context.object.pose and context.selected_pose_bones_from_active_object ) class LocalPoseAssetUser: @classmethod def poll(cls, context: Context) -> bool: return bool( isinstance(getattr(context, "id", None), Action) and context.object and context.object.mode == "POSE" # This condition may not be desired. ) class POSELIB_OT_create_pose_asset(PoseAssetCreator, Operator): bl_idname = "poselib.create_pose_asset" bl_label = "Create Pose Asset" bl_description = ( "Create a new Action that contains the pose of the selected bones, and mark it as Asset" ) bl_options = {"REGISTER", "UNDO"} pose_name: StringProperty(name="Pose Name") # type: ignore activate_new_action: BoolProperty(name="Activate New Action", default=True) # type: ignore def execute(self, context: Context) -> Set[str]: pose_name = self.pose_name or context.object.name 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"} if self.activate_new_action: self._set_active_action(context, asset) self._activate_asset_in_browser(context, asset) return {'FINISHED'} def _set_active_action(self, context: Context, asset: Action) -> None: self._prevent_action_loss(context.object) anim_data = context.object.animation_data_create() context.window_manager.poselib_previous_action = anim_data.action anim_data.action = asset 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_for_category( context.screen, "ANIMATION" ) 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_tags_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) def _prevent_action_loss(self, object: Object) -> None: """Mark the action with Fake User if necessary. This is to prevent action loss when we reduce its reference counter by one. """ if not object.animation_data: return action = object.animation_data.action if not action: return if action.use_fake_user or action.users > 1: # Removing one user won't GC it. return action.use_fake_user = True self.report({'WARNING'}, "Action %s marked Fake User to prevent loss" % action.name) class POSELIB_OT_restore_previous_action(Operator): bl_idname = "poselib.restore_previous_action" bl_label = "Restore Previous Action" bl_description = "Switch back to the previous Action, after creating a pose asset" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context: Context) -> bool: return bool( context.window_manager.poselib_previous_action and context.object and context.object.animation_data and context.object.animation_data.action and context.object.animation_data.action.asset_data is not None ) def execute(self, context: Context) -> Set[str]: # This is the Action that was just created with "Create Pose Asset". # It has to be re-applied after switching to the previous action, # to ensure the character keeps the same pose. self.pose_action = context.object.animation_data.action prev_action = context.window_manager.poselib_previous_action context.object.animation_data.action = prev_action context.window_manager.poselib_previous_action = None # Wait a bit for the action assignment to be handled, before applying the pose. wm = context.window_manager self._timer = wm.event_timer_add(0.001, window=context.window) wm.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): if event.type != 'TIMER': return {'RUNNING_MODAL'} wm = context.window_manager wm.event_timer_remove(self._timer) context.object.pose.apply_pose_from_action(self.pose_action) return {'FINISHED'} class ASSET_OT_assign_action(LocalPoseAssetUser, Operator): bl_idname = "asset.assign_action" bl_label = "Assign Action" bl_description = "Set this pose Action as active Action on the active Object" bl_options = {"REGISTER", "UNDO"} def execute(self, context: Context) -> Set[str]: context.object.animation_data_create().action = context.id return {"FINISHED"} class POSELIB_OT_copy_as_asset(PoseAssetCreator, Operator): bl_idname = "poselib.copy_as_asset" bl_label = "Copy Pose As Asset" bl_description = "Create a new pose asset on the clipboard, to be pasted into an Asset Browser" bl_options = {"REGISTER"} CLIPBOARD_ASSET_MARKER = "ASSET-BLEND=" def execute(self, context: Context) -> Set[str]: asset = pose_creation.create_pose_asset_from_context( context, new_asset_name=context.object.name ) if asset is None: self.report({"WARNING"}, "No animation data found to create asset from") return {"CANCELLED"} filepath = self.save_datablock(asset) functions.asset_clear(context, asset) if asset.users > 0: self.report({"ERROR"}, "Whaaaat who is using our brand new asset?") return {"FINISHED"} bpy.data.actions.remove(asset) context.window_manager.clipboard = "%s%s" % ( self.CLIPBOARD_ASSET_MARKER, filepath, ) asset_browser.tag_redraw(context.screen) self.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste") return {"FINISHED"} def save_datablock(self, action: Action) -> Path: tempdir = Path(bpy.app.tempdir) filepath = tempdir / "copied_asset.blend" bpy.data.libraries.write( str(filepath), datablocks={action}, path_remap="NONE", fake_user=True, compress=True, # Single-datablock blend file, likely little need to diff. ) return filepath class POSELIB_OT_paste_asset(Operator): bl_idname = "poselib.paste_asset" bl_label = "Paste As New Asset" bl_description = "Paste the Asset that was previously copied using Copy As Asset" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context: Context) -> bool: clipboard: str = context.window_manager.clipboard if not clipboard: return False marker = POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER return clipboard.startswith(marker) def execute(self, context: Context) -> Set[str]: clipboard = context.window_manager.clipboard marker_len = len(POSELIB_OT_copy_as_asset.CLIPBOARD_ASSET_MARKER) filepath = Path(clipboard[marker_len:]) assets = functions.load_assets_from(filepath) if not assets: self.report({"ERROR"}, "Did not find any assets on clipboard") return {"CANCELLED"} self.report({"INFO"}, "Pasted %d assets" % len(assets)) bpy.ops.file.refresh() asset_browser_area = asset_browser.area_from_context(context, 'ANIMATIONS') if asset_browser_area: asset_browser.activate_asset(assets[0], asset_browser_area, deferred=True) return {"FINISHED"} class PoseAssetUser: @classmethod def poll(cls, context: Context) -> bool: if not ( context.object and context.object.mode == "POSE" # This condition may not be desired. and context.asset_library and context.asset_file_handle ): return False return context.asset_file_handle.id_type == 'ACTION' def execute(self, context: Context) -> Set[str]: asset: FileSelectEntry = context.asset_file_handle if asset.local_id: return self.use_pose(context, asset.local_id) return self._load_and_use_pose(context) def use_pose(self, context: Context, asset: bpy.types.ID) -> Set[str]: # Implement in subclass. pass def _load_and_use_pose(self, context: Context) -> Set[str]: asset_library = context.asset_library asset = context.asset_file_handle asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset, asset_library) if not asset_lib_path: self.report( # type: ignore {"ERROR"}, # TODO: Add some way to get the library name from the library reference (just asset_library.name?). f"Selected asset {asset.name} could not be located inside the asset library", ) return {"CANCELLED"} if asset.id_type != 'ACTION': self.report( # type: ignore {"ERROR"}, f"Selected asset {asset.name} is not an Action", ) return {"CANCELLED"} with bpy.types.BlendData.temp_data() as temp_data: with temp_data.libraries.load(asset_lib_path) as (data_from, data_to): data_to.actions = [asset.name] action: Action = data_to.actions[0] return self.use_pose(context, action) class POSELIB_OT_pose_asset_select_bones(PoseAssetUser, Operator): bl_idname = "poselib.pose_asset_select_bones" bl_label = "Select Bones" bl_description = "Select those bones that are used in this pose" bl_options = {"REGISTER", "UNDO"} select: BoolProperty(name="Select", default=True) # type: ignore 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) verb = "Selected" if self.select else "Deselected" self.report({"INFO"}, f"{verb} bones from {pose_asset.name}") return {"FINISHED"} @classmethod def description( cls, _context: Context, properties: 'POSELIB_OT_pose_asset_select_bones' ) -> str: if properties.select: return cls.bl_description return cls.bl_description.replace("Select", "Deselect") class POSELIB_OT_blend_pose_asset_for_keymap(Operator): bl_idname = "poselib.blend_pose_asset_for_keymap" bl_options = {"REGISTER", "UNDO"} _rna = bpy.ops.poselib.blend_pose_asset.get_rna_type() bl_label = _rna.name bl_description = _rna.description del _rna @classmethod def poll(cls, context: Context) -> bool: return bpy.ops.poselib.blend_pose_asset.poll(context.copy()) def execute(self, context: Context) -> Set[str]: flipped = context.window_manager.poselib_flipped return bpy.ops.poselib.blend_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=flipped) def invoke(self, context: Context, event: Event) -> Set[str]: flipped = context.window_manager.poselib_flipped return bpy.ops.poselib.blend_pose_asset(context.copy(), 'INVOKE_DEFAULT', flipped=flipped) class POSELIB_OT_apply_pose_asset_for_keymap(Operator): bl_idname = "poselib.apply_pose_asset_for_keymap" bl_options = {"REGISTER", "UNDO"} _rna = bpy.ops.poselib.apply_pose_asset.get_rna_type() bl_label = _rna.name bl_description = _rna.description del _rna @classmethod def poll(cls, context: Context) -> bool: return bpy.ops.poselib.apply_pose_asset.poll(context.copy()) def execute(self, context: Context) -> Set[str]: flipped = context.window_manager.poselib_flipped return bpy.ops.poselib.apply_pose_asset(context.copy(), 'EXEC_DEFAULT', flipped=flipped) class POSELIB_OT_convert_old_poselib(Operator): bl_idname = "poselib.convert_old_poselib" bl_label = "Convert old-style pose library" bl_description = "Create a pose asset for each pose marker in the current action" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context: Context) -> bool: action = context.object and context.object.animation_data and context.object.animation_data.action if not action: cls.poll_message_set("Active object has no Action") return False if not action.pose_markers: cls.poll_message_set("Action %r is not a old-style pose library" % action.name) return False return True def execute(self, context: Context) -> Set[str]: from . import conversion old_poselib = context.object.animation_data.action new_actions = conversion.convert_old_poselib(old_poselib) if not new_actions: self.report({'ERROR'}, "Unable to convert to pose assets") return {'CANCELLED'} self.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions)) return {'FINISHED'} classes = ( ASSET_OT_assign_action, POSELIB_OT_apply_pose_asset_for_keymap, POSELIB_OT_blend_pose_asset_for_keymap, POSELIB_OT_convert_old_poselib, POSELIB_OT_copy_as_asset, POSELIB_OT_create_pose_asset, POSELIB_OT_paste_asset, POSELIB_OT_pose_asset_select_bones, POSELIB_OT_restore_previous_action, ) register, unregister = bpy.utils.register_classes_factory(classes)