Created
March 12, 2025 09:02
-
-
Save Shaun-Fong/9489473c04f52132f7d0ee81f0476df1 to your computer and use it in GitHub Desktop.
Blender Action Manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| import os | |
| bl_info = { | |
| "name": "Action Manager", | |
| "author": "ShaunFong", | |
| "version": (1, 0, 0), | |
| "location": "View3D > UI > Action Manager", | |
| "description": "Manage and append actions from external .blend files and apply them to selected armatures.", | |
| "category": "Animation", | |
| } | |
| class ActionNameProperty(bpy.types.PropertyGroup): # Define a PropertyGroup for Action Names | |
| name: bpy.props.StringProperty(name="Action Name") | |
| class BlendFileData(bpy.types.PropertyGroup): # Define a custom PropertyGroup for Blend File Data | |
| blend_file_name: bpy.props.StringProperty(name="Blend File Name") | |
| actions: bpy.props.CollectionProperty(type=ActionNameProperty, name="Actions") # Store actions as CollectionProperty of ActionNameProperty | |
| is_expanded: bpy.props.BoolProperty(name="Expanded", default=False) # Add is_expanded property to BlendFileData | |
| class ACTIONMANAGER_PT_MainPanel(bpy.types.Panel): | |
| bl_label = "Action Manager" | |
| bl_idname = "ACTIONMANAGER_PT_main" | |
| bl_space_type = "VIEW_3D" | |
| bl_region_type = "UI" | |
| bl_category = "Action Manager" | |
| def draw(self, context): | |
| layout = self.layout | |
| scene = context.scene | |
| # Folder input (auto-loads actions when changed) | |
| layout.prop(scene, "action_manager_folder", text="Folder") | |
| layout.operator("actionmanager.refresh_actions", text="Refresh Actions", icon='FILE_REFRESH') | |
| actions_by_blendfile = scene.action_manager_actions_data # Get action data | |
| if not actions_by_blendfile: | |
| layout.label(text="No actions loaded.", icon='INFO') | |
| return | |
| for blend_file_data in actions_by_blendfile: | |
| box = layout.box() | |
| row = box.row(align=True) | |
| row.prop(blend_file_data, 'is_expanded', text="", emboss=True, icon_only=False, expand=False) | |
| row.label(text=blend_file_data.blend_file_name, icon='FILE_BLEND') # Access property directly | |
| open_blend_op = row.operator("actionmanager.open_blend_file", text="", icon='FILE_FOLDER', emboss=True) | |
| open_blend_op.blend_file_name = blend_file_data.blend_file_name # Access property directly | |
| if blend_file_data.is_expanded: # Access property directly | |
| col = box.column(align=True) | |
| for action_name_prop in blend_file_data.actions: # Iterate through CollectionProperty of ActionNameProperty | |
| action_name = action_name_prop.name # Access StringProperty's name | |
| row = col.row(align=True) | |
| row.label(text=action_name, icon='ACTION') | |
| op = row.operator("actionmanager.append_action", text="Load", icon='IMPORT', emboss=True) | |
| op.action_name = action_name | |
| op.blend_file_name = blend_file_data.blend_file_name # Access property directly | |
| class ACTIONMANAGER_OT_LoadActions(bpy.types.Operator): | |
| bl_idname = "actionmanager.load_actions" | |
| bl_label = "Load Actions" | |
| def execute(self, context): | |
| scene = context.scene | |
| folder_path = scene.action_manager_folder | |
| if not folder_path or not os.path.isdir(folder_path): | |
| scene.action_manager_folder = "" | |
| scene.action_manager_actions_data.clear() | |
| self.report({'WARNING'}, "Invalid folder path. Actions list cleared.") | |
| return {'CANCELLED'} | |
| scene.action_manager_actions_data.clear() | |
| actions_found = False | |
| blend_files_data = [] | |
| for root, _, files in os.walk(folder_path): | |
| for file_name in files: | |
| if file_name.endswith(".blend"): | |
| file_path = os.path.join(root, file_name) | |
| blend_file_base_name = os.path.splitext(file_name)[0] | |
| blend_file_actions_names = [] | |
| with bpy.data.libraries.load(file_path, link=False) as (data_from, data_to): | |
| if not data_from.actions: | |
| continue | |
| for action_name in data_from.actions: | |
| blend_file_actions_names.append(action_name) | |
| actions_found = True | |
| if blend_file_actions_names: | |
| new_blend_file_data = scene.action_manager_actions_data.add() | |
| new_blend_file_data.blend_file_name = blend_file_base_name | |
| for action_name in blend_file_actions_names: # Populate actions CollectionProperty correctly | |
| action_prop = new_blend_file_data.actions.add() # Add ActionNameProperty item | |
| action_prop.name = action_name # Set the name property of ActionNameProperty | |
| new_blend_file_data.is_expanded = False | |
| if not actions_found: | |
| self.report({'WARNING'}, "No actions found in selected folder.") | |
| return {'CANCELLED'} | |
| return {'FINISHED'} | |
| class ACTIONMANAGER_OT_AppendAction(bpy.types.Operator): | |
| bl_idname = "actionmanager.append_action" | |
| bl_label = "Append and Apply Action" | |
| action_name: bpy.props.StringProperty(name="Action Name") | |
| blend_file_name: bpy.props.StringProperty(name="Blend File Name") | |
| def execute(self, context): | |
| blend_file_path = "" | |
| folder_path = context.scene.action_manager_folder | |
| blend_file_name = self.blend_file_name | |
| action_name = self.action_name | |
| for root, _, files in os.walk(folder_path): | |
| for file_name in files: | |
| if file_name == blend_file_name + ".blend": | |
| blend_file_path = os.path.join(root, file_name) | |
| break | |
| if blend_file_path: | |
| break | |
| if not blend_file_path: | |
| self.report({'ERROR'}, f"Blend file '{blend_file_name}.blend' not found in folder.") | |
| return {'CANCELLED'} | |
| try: | |
| with bpy.data.libraries.load(blend_file_path, link=False) as (data_from, data_to): | |
| if action_name in data_from.actions: | |
| data_to.actions = [action_name] | |
| else: | |
| self.report({'ERROR'}, f"Action '{action_name}' not found in {blend_file_name}.blend") | |
| return {'CANCELLED'} | |
| except Exception as e: | |
| self.report({'ERROR'}, str(e)) | |
| return {'CANCELLED'} | |
| obj = context.object | |
| if obj and obj.type == 'ARMATURE': | |
| obj.animation_data_create() | |
| obj.animation_data.action = bpy.data.actions.get(action_name) | |
| self.report({'INFO'}, f"Applied action: {action_name} from {blend_file_name} to {obj.name}") | |
| else: | |
| self.report({'WARNING'}, "No armature selected. Action appended but not applied.") | |
| return {'FINISHED'} | |
| class ACTIONMANAGER_OT_RefreshActions(bpy.types.Operator): | |
| bl_idname = "actionmanager.refresh_actions" | |
| bl_label = "Refresh Actions" | |
| def execute(self, context): | |
| scene = context.scene | |
| scene.action_manager_actions_data.clear() | |
| load_result = bpy.ops.actionmanager.load_actions() | |
| if load_result == {'CANCELLED'}: | |
| self.report({'WARNING'}, "No actions found in selected folder.") | |
| else: | |
| self.report({'INFO'}, "Action list refreshed.") | |
| return {'FINISHED'} | |
| class ACTIONMANAGER_OT_OpenBlendFile(bpy.types.Operator): | |
| bl_idname = "actionmanager.open_blend_file" | |
| bl_label = "Open Blend File" | |
| blend_file_name: bpy.props.StringProperty(name="Blend File Name") | |
| def execute(self, context): | |
| folder_path = context.scene.action_manager_folder | |
| blend_file_name = self.blend_file_name | |
| blend_file_path = "" | |
| if not folder_path or not os.path.isdir(folder_path): | |
| self.report({'WARNING'}, "Invalid folder path set in Action Manager.") | |
| return {'CANCELLED'} | |
| for root, _, files in os.walk(folder_path): | |
| for file_name in files: | |
| if file_name == blend_file_name + ".blend": | |
| blend_file_path = os.path.join(root, file_name) | |
| break | |
| if blend_file_path: | |
| break | |
| if blend_file_path: | |
| try: | |
| bpy.ops.wm.open_mainfile(filepath=blend_file_path) | |
| return {'FINISHED'} | |
| except Exception as e: | |
| self.report({'ERROR'}, f"Could not open blend file: {blend_file_name}.blend\n{str(e)}") | |
| return {'CANCELLED'} | |
| else: | |
| self.report({'WARNING'}, f"Blend file '{blend_file_name}.blend' not found in folder.") | |
| return {'CANCELLED'} | |
| def register(): | |
| # Removed registration of ActionListItem and ACTIONMANAGER_UL_ActionList | |
| bpy.utils.register_class(ActionNameProperty) # Register ActionNameProperty | |
| bpy.utils.register_class(BlendFileData) # Register BlendFileData PropertyGroup | |
| bpy.utils.register_class(ACTIONMANAGER_PT_MainPanel) | |
| bpy.utils.register_class(ACTIONMANAGER_OT_LoadActions) | |
| bpy.utils.register_class(ACTIONMANAGER_OT_AppendAction) | |
| bpy.utils.register_class(ACTIONMANAGER_OT_RefreshActions) | |
| bpy.utils.register_class(ACTIONMANAGER_OT_OpenBlendFile) | |
| bpy.types.Scene.action_manager_folder = bpy.props.StringProperty( | |
| name="Folder Path", subtype='DIR_PATH', | |
| update=lambda self, context: bpy.ops.actionmanager.load_actions() | |
| ) | |
| # Changed to a CollectionProperty of BlendFileData PropertyGroup | |
| bpy.types.Scene.action_manager_actions_data = bpy.props.CollectionProperty(type=BlendFileData, name="Action Data") | |
| def unregister(): | |
| bpy.utils.unregister_class(ActionNameProperty) # Unregister ActionNameProperty | |
| bpy.utils.unregister_class(BlendFileData) # Unregister BlendFileData PropertyGroup | |
| bpy.utils.unregister_class(ACTIONMANAGER_PT_MainPanel) | |
| bpy.utils.unregister_class(ACTIONMANAGER_OT_LoadActions) | |
| bpy.utils.unregister_class(ACTIONMANAGER_OT_AppendAction) | |
| bpy.utils.unregister_class(ACTIONMANAGER_OT_RefreshActions) | |
| bpy.utils.unregister_class(ACTIONMANAGER_OT_OpenBlendFile) | |
| del bpy.types.Scene.action_manager_folder | |
| del bpy.types.Scene.action_manager_actions_data | |
| if __name__ == "__main__": | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment