├── README.md ├── v2.5 ├── clean_up_utilities.py └── mesh_modifiers_utilities.py ├── v2.9 ├── armature_utilities.py └── object_utilities.py └── v3.0 ├── armature_utilities.py └── object_utilities.py /README.md: -------------------------------------------------------------------------------- 1 | # blender-scripts 2 | Blender utility scripts for various versions. 3 | 4 | ## Scripts for Blender 3.0, in /v3.0 5 | 6 | ### object_utilities.py 7 | * View the difference between two objects' data (Custom properties, vertex groups, vertex colors, modifiers) 8 | * Synchronize two objects' custom properties (removes properties existing only in the target object) 9 | * Copy all custom property values from one object to another (only for properties existing in both objects) 10 | * Make all custom properties, in selected object, "library overridable" 11 | * Remove, from selected objects, all vertex groups that contain only zero values (below 0.01). 12 | * Remove all modifiers of selected objects 13 | * Remove location keyframes of selected objects (and its bones if armature) for the current frame time 14 | * Remove rotation keyframes of selected objects (and its bones if armature) for the current frame time 15 | * Remove scale keyframes of selected objects (and its bones if armature) for the current frame time 16 | 17 | ## Scripts for Blender 2.9, in /v2.9 18 | 19 | ### armature_utilities.py 20 | * Refresh the constraints and properties of an armature proxy that links to another blender file. (Does not work with NLA, must fix) 21 | * Copy all bone constraints from one armature to another, using bone names for matching. 22 | 23 | ### object_utilities.py 24 | * View the difference between two objects' data (Custom properties, vertex groups, vertex colors, modifiers) 25 | * Synchronize two objects' custom properties (removes properties existing only in the target object) 26 | * Remove, from selected objects, all vertex groups that contain only zero values (below 0.01). 27 | * Remove all modifiers of selected objects 28 | * Remove location keyframes of selected objects (and its bones if armature) for the current frame time 29 | * Remove rotation keyframes of selected objects (and its bones if armature) for the current frame time 30 | * Remove scale keyframes of selected objects (and its bones if armature) for the current frame time 31 | 32 | ## Scripts for Blender 2.5, in /v2.5 33 | 34 | ### clean_up_utilities.py 35 | * Removes all unused materials and images. 36 | 37 | ### mesh_modifiers_utilities.py 38 | This scripts enables the following operations: 39 | * Copy the modifiers from an object to another 40 | * Remove all modifiers from an object 41 | * Toggle on/off all modifiers of an object in the viewport 42 | * Toggle on/off specified modifiers of an object in the viewport 43 | * Modifer names must be listed in the object's property named "Modifiers" 44 | -------------------------------------------------------------------------------- /v2.5/clean_up_utilities.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------- 2 | # Clean-up Utilities (author Jango73) 3 | # - Remove unused materials and images 4 | # -------------------------------------------------------------------------- 5 | # ***** BEGIN GPL LICENSE BLOCK ***** 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 2 10 | # of the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software Foundation, 19 | # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 20 | # 21 | # ***** END GPL LICENCE BLOCK ***** 22 | # -------------------------------------------------------------------------- 23 | 24 | bl_info = { 25 | "name": "Clean-up utilities", 26 | "author": "Jango73", 27 | "version": (1, 0), 28 | "blender": (2, 57, 0), 29 | "location": "Tools > Clean-up utilities", 30 | "description": "Provides utilities for scene clean-up", 31 | "warning": "", 32 | "wiki_url": "" 33 | "", 34 | "category": "3D View", 35 | } 36 | 37 | """ 38 | Usage: 39 | 40 | Launch from "Tools -> Clean-up utilities" 41 | 42 | 43 | Additional links: 44 | e-mail: therealjango73 {at} gmail {dot} com 45 | """ 46 | 47 | import bpy 48 | import bmesh 49 | from bpy.props import IntProperty 50 | from bpy.types import Operator, Panel 51 | 52 | def clean_up_images(self, context): 53 | # iterate over all materials in the file 54 | for material in bpy.data.materials: 55 | 56 | # don't do anything if the material has any users. 57 | if material.users: 58 | continue 59 | 60 | # remove the material otherwise 61 | bpy.data.materials.remove(material) 62 | 63 | # iterate over all images in the file 64 | for image in bpy.data.images: 65 | 66 | # don't do anything if the image has any users. 67 | if image.users: 68 | continue 69 | 70 | # remove the image otherwise 71 | bpy.data.images.remove(image) 72 | 73 | return {'FINISHED'} 74 | 75 | class CleanUpImages(bpy.types.Operator): 76 | """Removes unused images in the scene""" 77 | bl_idname = 'mesh.clean_up_images' 78 | bl_label = 'Clean-up images' 79 | bl_options = {'REGISTER', 'UNDO'} 80 | 81 | def execute(self, context): 82 | return clean_up_images(self, context) 83 | 84 | class c_clean_up_utilities(Panel): 85 | bl_space_type = 'VIEW_3D' 86 | bl_region_type = 'TOOLS' 87 | bl_category = 'Tools' 88 | bl_label = "Clean-up utilities" 89 | bl_context = "objectmode" 90 | bl_options = {'DEFAULT_CLOSED'} 91 | 92 | def draw(self, context): 93 | layout = self.layout 94 | layout.operator(CleanUpImages.bl_idname, text="Clean-up images") 95 | 96 | def register(): 97 | bpy.utils.register_module(__name__) 98 | 99 | def unregister(): 100 | bpy.utils.unregister_module(__name__) 101 | 102 | if __name__ == "__main__": 103 | register() 104 | -------------------------------------------------------------------------------- /v2.5/mesh_modifiers_utilities.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------- 2 | # Mesh Modifiers Utilities (author Jango73) 3 | # - Copy the modifiers from a mesh to another 4 | # - Remove all modifiers from a mesh 5 | # - Toggle on/off all modifiers 6 | # - Toggle on/off specified modifiers (in a custom property named Modifiers) 7 | # -------------------------------------------------------------------------- 8 | # ***** BEGIN GPL LICENSE BLOCK ***** 9 | # 10 | # This program is free software; you can redistribute it and/or 11 | # modify it under the terms of the GNU General Public License 12 | # as published by the Free Software Foundation; either version 2 13 | # of the License, or (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software Foundation, 22 | # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 23 | # 24 | # ***** END GPL LICENCE BLOCK ***** 25 | # -------------------------------------------------------------------------- 26 | 27 | bl_info = { 28 | "name": "Modifier utilities", 29 | "author": "Jango73", 30 | "version": (1, 0), 31 | "blender": (2, 57, 0), 32 | "location": "Tools > Modifier utilities", 33 | "description": "Provides utilities for modifiers", 34 | "warning": "", 35 | "wiki_url": "" 36 | "", 37 | "category": "Mesh", 38 | } 39 | 40 | """ 41 | Usage: 42 | 43 | Launch from "Tools -> Modifier utilities" 44 | 45 | 46 | Additional links: 47 | Author Site: 48 | e-mail: therealjango73 {at} gmail {dot} com 49 | """ 50 | 51 | import bpy 52 | import bmesh 53 | from bpy.props import IntProperty 54 | from bpy.types import Operator, Panel 55 | 56 | # The following function is adapted from 57 | # Nick Keeline "Cloud Generator" addNewObject 58 | # from object_cloud_gen.py (an addon that comes with the Blender 2.6 package) 59 | # 60 | def duplicateObject(scene, name, copyobj): 61 | 62 | # Create new mesh 63 | # mesh = bpy.data.meshes.new(name) 64 | 65 | # Create new object associated with the mesh 66 | # ob_new = bpy.data.objects.new(name, mesh) 67 | ob_new = copyobj.copy() 68 | 69 | # Copy data block from the old object into the new object 70 | # ob_new.data = copyobj.data.copy() 71 | ob_new.data = bpy.data.meshes.new(name) 72 | ob_new.scale = copyobj.scale 73 | ob_new.location = copyobj.location 74 | ob_new.rotation_euler = copyobj.rotation_euler 75 | 76 | # Link new object to the given scene and select it 77 | scene.objects.link(ob_new) 78 | ob_new.select = True 79 | 80 | # for mod in copyobj.modifiers: 81 | # ob_new.modifiers.append(mod) 82 | 83 | return ob_new 84 | 85 | def mesh_copy_modifiers(self, context): 86 | source = None 87 | 88 | # deselect everything that's not related 89 | # for obj in context.selected_objects: 90 | source = context.selected_objects[0] 91 | 92 | if source is None: 93 | self.report({'WARNING'}, "You must select a source object") 94 | return {'CANCELLED'} 95 | 96 | # get active object 97 | target = context.active_object 98 | 99 | if target is None: 100 | self.report({'WARNING'}, "No active object...") 101 | return {'CANCELLED'} 102 | 103 | if target == source: 104 | # self.report({'WARNING'}, "Source and target is same object...") 105 | # return {'CANCELLED'} 106 | source = context.selected_objects[1] 107 | 108 | # copy the source object 109 | name = target.name 110 | source = duplicateObject(context.scene, name, source) 111 | target.name = target.name + "_old" 112 | source.name = name 113 | 114 | # remove all materials from source object 115 | while source.data.materials: 116 | source.data.materials.pop(0, update_data=True) 117 | 118 | # remove all modifiers from target object 119 | while target.modifiers: 120 | target.modifiers.remove(target.modifiers[0]) 121 | 122 | bpy.ops.object.select_all(action='DESELECT') 123 | target.select = True 124 | context.scene.objects.active = source 125 | source.select = True 126 | bpy.ops.object.join() 127 | 128 | return {'FINISHED'} 129 | 130 | class MeshCopyModifiers(bpy.types.Operator): 131 | """Copies modifiers from a mesh to another """ 132 | bl_idname = 'mesh.mesh_copy_modifiers' 133 | bl_label = 'Copy mesh modifiers' 134 | bl_options = {'REGISTER', 'UNDO'} 135 | 136 | def execute(self, context): 137 | return mesh_copy_modifiers(self, context) 138 | 139 | class MeshRemoveModifiers(bpy.types.Operator): 140 | """Removes all modifiers from selected object""" 141 | bl_idname = 'mesh.mesh_remove_modifiers' 142 | bl_label = 'Remove modifiers' 143 | bl_options = {'REGISTER', 'UNDO'} 144 | 145 | def execute(self, context): 146 | for target in context.selected_objects: 147 | for idx, const in enumerate(target.modifiers): 148 | target.modifiers.remove(const) 149 | return {'FINISHED'} 150 | 151 | class MeshToggleSpecifiedModifiers(bpy.types.Operator): 152 | """Toggles 3d view display of modifiers specified in object's custom property Modifiers. Write comma-separated names of modifiers to toggle""" 153 | bl_idname = 'mesh.mesh_toggle_specified_modifiers' 154 | bl_label = 'Toggle specified modifiers' 155 | bl_options = {'REGISTER', 'UNDO'} 156 | 157 | def execute(self, context): 158 | for target in context.selected_objects: 159 | text = target["Modifiers"] 160 | if text: 161 | for name in text.split(','): 162 | for idx, const in enumerate(target.modifiers): 163 | if const.name == name: 164 | const.show_viewport = not const.show_viewport 165 | return {'FINISHED'} 166 | 167 | class MeshToggleAllModifiers(bpy.types.Operator): 168 | """Toggles 3d view display of all modifiers.""" 169 | bl_idname = 'mesh.mesh_toggle_all_modifiers' 170 | bl_label = 'Toggle all modifiers' 171 | bl_options = {'REGISTER', 'UNDO'} 172 | 173 | def execute(self, context): 174 | for target in context.selected_objects: 175 | for idx, const in enumerate(target.modifiers): 176 | const.show_viewport = not const.show_viewport 177 | return {'FINISHED'} 178 | 179 | class c_mesh_modifiers_utilities(Panel): 180 | bl_space_type = 'VIEW_3D' 181 | bl_region_type = 'TOOLS' 182 | bl_category = 'Tools' 183 | bl_label = "Modifier utilities" 184 | bl_context = "objectmode" 185 | bl_options = {'DEFAULT_CLOSED'} 186 | 187 | def draw(self, context): 188 | layout = self.layout 189 | layout.operator(MeshCopyModifiers.bl_idname, text="Copy mesh modifiers") 190 | layout.operator(MeshRemoveModifiers.bl_idname, text="Remove all modifiers") 191 | layout.operator(MeshToggleAllModifiers.bl_idname, text="Toggle all modifiers") 192 | layout.operator(MeshToggleSpecifiedModifiers.bl_idname, text="Toggle specified modifiers") 193 | 194 | def register(): 195 | bpy.utils.register_module(__name__) 196 | 197 | def unregister(): 198 | bpy.utils.unregister_module(__name__) 199 | 200 | if __name__ == "__main__": 201 | register() 202 | -------------------------------------------------------------------------------- /v2.9/armature_utilities.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Armature utilities", 21 | "author": "Jango73", 22 | "version": (1, 0), 23 | "blender": (2, 80, 0), 24 | "description": "Operations on armatures", 25 | "category": "Object", 26 | } 27 | 28 | import bpy 29 | 30 | # ----------------------------------------------------------------------------- 31 | 32 | def copyPose(context, source, target): 33 | if bpy.ops.object.mode_set.poll(): 34 | context.view_layer.objects.active = source 35 | bpy.ops.object.mode_set(mode='POSE') 36 | for b in source.data.bones: 37 | b.select = True 38 | bpy.ops.pose.copy() 39 | bpy.ops.object.mode_set(mode='OBJECT') 40 | 41 | context.view_layer.objects.active = target 42 | bpy.ops.object.mode_set(mode='POSE') 43 | for b in target.data.bones: 44 | b.select = True 45 | bpy.ops.pose.paste() 46 | bpy.ops.object.mode_set(mode='OBJECT') 47 | 48 | def refreshArmatureProxy(context): 49 | source = context.selected_objects[0] 50 | 51 | if source is None: 52 | return {'CANCELLED'} 53 | 54 | coll = source.users_collection[0] 55 | old_proxy_object = source.copy() 56 | old_proxy_object.name = "foobar" 57 | coll.objects.link(old_proxy_object) 58 | 59 | old_proxy_show_in_front = old_proxy_object.show_in_front 60 | current_frame = context.scene.frame_current 61 | 62 | bones = source.proxy 63 | bones_collection = source.proxy_collection 64 | bones_collection_hide_viewport = bones_collection.hide_viewport 65 | bones_collection.hide_viewport = False 66 | old_proxy_object.select_set(False) 67 | bpy.ops.object.delete() 68 | 69 | context.view_layer.objects.active = bones_collection 70 | target_proxy_object = bpy.ops.object.proxy_make(object=bones.name) 71 | target_proxy_object = context.view_layer.objects.active 72 | 73 | target_proxy_object.animation_data_create() 74 | target_proxy_object.animation_data.action = old_proxy_object.animation_data.action 75 | 76 | bones_collection.hide_viewport = bones_collection_hide_viewport 77 | bones_collection.select_set(False) 78 | 79 | target_proxy_object.show_in_front = old_proxy_show_in_front 80 | target_proxy_object.select_set(False) 81 | 82 | # Copy properties from old proxy to new proxy 83 | # (Only those existing in new proxy) 84 | for p in old_proxy_object.keys(): 85 | if not p.startswith("_"): 86 | if p in target_proxy_object.keys(): 87 | target_proxy_object[p] = old_proxy_object[p] 88 | 89 | old_proxy_object.select_set(True) 90 | bpy.ops.object.delete() 91 | 92 | target_proxy_object.hide_viewport = True 93 | target_proxy_object.hide_render = True 94 | 95 | target_proxy_object.hide_viewport = False 96 | target_proxy_object.hide_render = False 97 | 98 | context.scene.frame_set(context.scene.frame_start) 99 | context.scene.frame_set(current_frame) 100 | 101 | target_proxy_object.select_set(True) 102 | 103 | return {'FINISHED'} 104 | 105 | def copyArmatureConstraints(self, context): 106 | target = context.selected_objects[0] 107 | 108 | if target is None: 109 | return {'CANCELLED'} 110 | 111 | # get active object 112 | source = context.active_object 113 | 114 | if source is None: 115 | return {'CANCELLED'} 116 | 117 | if target == source: 118 | target = context.selected_objects[1] 119 | 120 | if target is None: 121 | return {'CANCELLED'} 122 | 123 | target.select_set(False) 124 | source.select_set(True) 125 | 126 | bpy.ops.object.posemode_toggle() 127 | bpy.ops.pose.select_all(action='SELECT') 128 | sourceBones = context.selected_pose_bones 129 | bpy.ops.pose.select_all(action='DESELECT') 130 | bpy.ops.object.posemode_toggle() 131 | 132 | for bone in sourceBones: 133 | 134 | bone1 = source.pose.bones[bone.name] 135 | bone2 = target.pose.bones[bone.name] 136 | 137 | if bone2 is not None: 138 | for constraint in bone2.constraints: 139 | bone2.constraints.remove(constraint) 140 | 141 | for constraint in bone1.constraints: 142 | bone2.constraints.copy(constraint) 143 | 144 | for constraint in bone2.constraints: 145 | if hasattr(constraint, 'target'): 146 | if constraint.target == source: 147 | constraint.target = target 148 | 149 | self.report({'INFO'}, "Copied " + source.name + " bone constraints to " + target.name) 150 | 151 | return {'FINISHED'} 152 | 153 | # ----------------------------------------------------------------------------- 154 | # Operators 155 | 156 | class OBJECT_OT_RefreshArmatureProxy(bpy.types.Operator): 157 | """Refresh Armature Proxy""" 158 | bl_idname = "object.refresh_armature_proxy" 159 | bl_label = "Refresh armature proxy" 160 | bl_description = "Refreshes the proxy for an armature that exists in a linked collection" 161 | bl_options = {'REGISTER'} 162 | 163 | def execute(self, context): 164 | return refreshArmatureProxy(context) 165 | 166 | class OBJECT_OT_CopyArmatureConstraints(bpy.types.Operator): 167 | """Copy Armature Constraints""" 168 | bl_idname = "object.copy_armature_constraints" 169 | bl_label = "Copy armature constraints" 170 | bl_description = "Copies the bone contraints from an armature to another" 171 | bl_options = {'REGISTER'} 172 | 173 | def execute(self, context): 174 | return copyArmatureConstraints(self, context) 175 | 176 | # ----------------------------------------------------------------------------- 177 | # Panels 178 | 179 | class OBJECT_PT_armature_utilities(bpy.types.Panel): 180 | bl_idname = "OBJECT_PT_armature_utilities" 181 | bl_label = "Armature Utilities" 182 | bl_space_type = 'VIEW_3D' 183 | bl_region_type = 'UI' 184 | bl_category = "Edit" 185 | bl_context = 'objectmode' 186 | 187 | @classmethod 188 | def poll(cls, context): 189 | return (context.object is not None) 190 | 191 | def draw(self, context): 192 | layout = self.layout 193 | 194 | layout.operator("object.refresh_armature_proxy") 195 | layout.operator("object.copy_armature_constraints") 196 | 197 | # ----------------------------------------------------------------------------- 198 | # Registering 199 | 200 | def register(): 201 | bpy.utils.register_class(OBJECT_OT_RefreshArmatureProxy) 202 | bpy.utils.register_class(OBJECT_OT_CopyArmatureConstraints) 203 | bpy.utils.register_class(OBJECT_PT_armature_utilities) 204 | 205 | def unregister(): 206 | bpy.utils.unregister_class(OBJECT_OT_RefreshArmatureProxy) 207 | bpy.utils.unregister_class(OBJECT_OT_CopyArmatureConstraints) 208 | bpy.utils.unregister_class(OBJECT_PT_armature_utilities) 209 | 210 | if __name__ == "__main__": 211 | register() 212 | -------------------------------------------------------------------------------- /v2.9/object_utilities.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Object utilities", 21 | "author": "Jango73", 22 | "version": (1, 2), 23 | "blender": (2, 80, 0), 24 | "description": "Operations on objects", 25 | "category": "Object", 26 | } 27 | 28 | import bpy 29 | import re 30 | import copy 31 | 32 | # ----------------------------------------------------------------------------- 33 | 34 | def showMessageBox(title = "Message Box", icon = 'INFO', lines=""): 35 | myLines=lines 36 | def draw(self, context): 37 | for n in myLines: 38 | self.layout.label(text=n) 39 | bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 40 | 41 | # ----------------------------------------------------------------------------- 42 | 43 | def printToString(targetString, text, no_newline=False): 44 | 45 | temp = targetString.split("\n") 46 | if len(temp) > 0: 47 | if len(temp[-1]) > 200: 48 | targetString += ("\n") 49 | 50 | if (no_newline): 51 | targetString += (text) 52 | else: 53 | targetString += (text + "\n") 54 | 55 | return targetString 56 | 57 | # ----------------------------------------------------------------------------- 58 | 59 | def diffLines(targetString, sourceName, targetName, lines1, lines2): 60 | temp = copy.deepcopy(lines1) 61 | 62 | for x in temp: 63 | if x in lines2: 64 | lines1.remove(x) 65 | lines2.remove(x) 66 | 67 | if len(lines1) == 0 and len(lines2) == 0: 68 | targetString = printToString(targetString, "No difference") 69 | return targetString 70 | 71 | if len(lines1) > 0: 72 | targetString = printToString(targetString, "Only in " + sourceName + ":") 73 | for x in lines1: 74 | targetString = printToString(targetString, x + " ", no_newline=True) 75 | targetString = printToString(targetString, "") 76 | 77 | if len(lines2) > 0: 78 | targetString = printToString(targetString, "Only in " + targetName + ":") 79 | for x in lines2: 80 | targetString = printToString(targetString, x + " ", no_newline=True) 81 | targetString = printToString(targetString, "") 82 | 83 | return targetString 84 | 85 | # ----------------------------------------------------------------------------- 86 | 87 | def diffObjects(self, context): 88 | lineCount = 0 89 | diffCount = 0 90 | sameCount = 0 91 | targetString = "" 92 | lines1 = [] 93 | lines2 = [] 94 | 95 | target = context.selected_objects[0] 96 | 97 | if target is None: 98 | return {'CANCELLED'} 99 | 100 | # get active object 101 | source = context.active_object 102 | 103 | if source is None: 104 | return {'CANCELLED'} 105 | 106 | if target == source: 107 | if len(context.selected_objects) < 2: 108 | return {'CANCELLED'} 109 | target = context.selected_objects[1] 110 | 111 | if target is None: 112 | return {'CANCELLED'} 113 | 114 | targetString = printToString(targetString, "") 115 | targetString = printToString(targetString, "Diff " + source.name + " and " + target.name) 116 | targetString = printToString(targetString, "") 117 | 118 | if "_RNA_UI" in source and "_RNA_UI" in target: 119 | targetString = printToString(targetString, "[ Custom properties ]") 120 | 121 | props1 = source["_RNA_UI"] 122 | props2 = target["_RNA_UI"] 123 | 124 | for p in props1.keys(): 125 | lines1.append(p.strip()) 126 | for p in props2.keys(): 127 | lines2.append(p.strip()) 128 | 129 | lines1.sort() 130 | lines2.sort() 131 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 132 | 133 | try: 134 | targetString = printToString(targetString, "") 135 | targetString = printToString(targetString, "[ Vertex groups ]") 136 | 137 | lines1.clear() 138 | lines2.clear() 139 | 140 | for g in source.vertex_groups: 141 | lines1.append(g.name.strip()) 142 | for g in target.vertex_groups: 143 | lines2.append(g.name.strip()) 144 | 145 | lines1.sort() 146 | lines2.sort() 147 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 148 | 149 | except: 150 | pass 151 | 152 | try: 153 | targetString = printToString(targetString, "") 154 | targetString = printToString(targetString, "[ Vertex colors ]") 155 | 156 | lines1.clear() 157 | lines2.clear() 158 | 159 | for v in source.data.vertex_colors.keys(): 160 | lines1.append(v.strip()) 161 | for v in target.data.vertex_colors.keys(): 162 | lines2.append(v.strip()) 163 | 164 | lines1.sort() 165 | lines2.sort() 166 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 167 | 168 | except: 169 | pass 170 | 171 | try: 172 | targetString = printToString(targetString, "") 173 | targetString = printToString(targetString, "[ Modifiers ]") 174 | 175 | lines1.clear() 176 | lines2.clear() 177 | 178 | for m in source.modifiers: 179 | lines1.append(m.name.strip()) 180 | for m in target.modifiers: 181 | lines2.append(m.name.strip()) 182 | 183 | lines1.sort() 184 | lines2.sort() 185 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 186 | 187 | except: 188 | pass 189 | 190 | try: 191 | objectType = getattr(source, 'type', '') 192 | 193 | if objectType in ['ARMATURE']: 194 | targetString = printToString(targetString, "") 195 | targetString = printToString(targetString, "[ Bone constraints ]") 196 | bpy.ops.object.mode_set(mode='POSE') 197 | 198 | lines1.clear() 199 | lines2.clear() 200 | 201 | for b in source.pose.bones: 202 | for c in b.constraints: 203 | lines1.append(b.name.strip() + ":" + c.name.strip()) 204 | for b in target.pose.bones: 205 | for c in b.constraints: 206 | lines2.append(b.name.strip() + ":" + c.name.strip()) 207 | 208 | bpy.ops.object.mode_set(mode='OBJECT') 209 | 210 | lines1.sort() 211 | lines2.sort() 212 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 213 | 214 | except: 215 | pass 216 | 217 | showMessageBox(lines=targetString.split("\n")) 218 | 219 | return {'FINISHED'} 220 | 221 | # ----------------------------------------------------------------------------- 222 | 223 | def getMirroredName(name): 224 | if '.R' in name: 225 | return name.replace('.R','.L') 226 | 227 | if '.r' in name: 228 | return name.replace('.r','.l') 229 | 230 | if name.startswith('Right'): 231 | return name.replace('Right', 'Left') 232 | 233 | if name.startswith('right'): 234 | return name.replace('right', 'left') 235 | 236 | return '' 237 | 238 | # ----------------------------------------------------------------------------- 239 | 240 | def syncObjectProperties(self, context): 241 | target = context.selected_objects[0] 242 | 243 | if target is None: 244 | return {'CANCELLED'} 245 | 246 | # get active object 247 | source = context.active_object 248 | 249 | if source is None: 250 | return {'CANCELLED'} 251 | 252 | if target == source: 253 | if len(context.selected_objects) < 2: 254 | return {'CANCELLED'} 255 | target = context.selected_objects[1] 256 | 257 | if target is None: 258 | return {'CANCELLED'} 259 | 260 | props = source["_RNA_UI"] 261 | 262 | for p in props.keys(): 263 | if not p.startswith("_"): 264 | if p not in target.keys(): 265 | target[p] = source[p] 266 | 267 | self.report({'INFO'}, "Synced " + target.name + " properties with " + source.name) 268 | 269 | return {'FINISHED'} 270 | 271 | # ----------------------------------------------------------------------------- 272 | 273 | def removeEmptyVertexGroups(self, context): 274 | for object in context.selected_objects: 275 | 276 | object.update_from_editmode() 277 | 278 | vgroup_used = {i: False for i, k in enumerate(object.vertex_groups)} 279 | vgroup_names = {i: k.name for i, k in enumerate(object.vertex_groups)} 280 | vgroup_name_list = list(vgroup_names.values()) 281 | 282 | for v in object.data.vertices: 283 | for g in v.groups: 284 | 285 | mirrored_name = getMirroredName(vgroup_names[g.group]) 286 | 287 | if mirrored_name in vgroup_name_list: 288 | vgroup_used[g.group] = vgroup_used[vgroup_name_list.index(mirrored_name)] 289 | else: 290 | if g.weight > 0.01: 291 | vgroup_used[g.group] = True 292 | 293 | for i, used in sorted(vgroup_used.items(), reverse=True): 294 | if not used: 295 | object.vertex_groups.remove(object.vertex_groups[i]) 296 | 297 | self.report({'INFO'}, "Removed empty groups from " + object.name) 298 | 299 | return {'FINISHED'} 300 | 301 | # ----------------------------------------------------------------------------- 302 | 303 | def removeAllModifiers(self, context): 304 | for object in context.selected_objects: 305 | 306 | # remove all modifiers 307 | object.modifiers.clear() 308 | 309 | self.report({'INFO'}, "Removed all modifiers from " + object.name) 310 | 311 | return {'FINISHED'} 312 | 313 | # ----------------------------------------------------------------------------- 314 | 315 | def removeKeyframesByChannel(self, context, channel): 316 | for object in context.selected_objects: 317 | 318 | if object.animation_data: 319 | action = object.animation_data.action 320 | if action: 321 | for fc in action.fcurves: 322 | if fc.data_path.endswith(channel): 323 | try: 324 | object.keyframe_delete(fc.data_path) 325 | except TypeError: 326 | print(fc.data_path + " channel does not exist. Ignoring.") 327 | 328 | self.report({'INFO'}, "Removed " + channel + " type keyframes from " + object.name) 329 | 330 | return {'FINISHED'} 331 | 332 | # ----------------------------------------------------------------------------- 333 | 334 | def cleanUpMaterialsAndImages(context): 335 | # iterate over all materials in the file 336 | for material in bpy.data.materials: 337 | 338 | # don't do anything if the material has any users. 339 | if material.users: 340 | continue 341 | 342 | # remove the material otherwise 343 | bpy.data.materials.remove(material) 344 | 345 | # iterate over all images in the file 346 | for image in bpy.data.images: 347 | 348 | # don't do anything if the image has any users. 349 | if image.users: 350 | continue 351 | 352 | # remove the image otherwise 353 | bpy.data.images.remove(image) 354 | 355 | return {'FINISHED'} 356 | 357 | # ----------------------------------------------------------------------------- 358 | # Operators 359 | 360 | class OBJECT_OT_DiffObjectData(bpy.types.Operator): 361 | """Diff Object Data""" 362 | bl_idname = "object.diff_object_data" 363 | bl_label = "Diff object data" 364 | bl_description = "Shows difference in object data" 365 | bl_options = {'REGISTER'} 366 | 367 | def execute(self, context): 368 | return diffObjects(self, context) 369 | 370 | class OBJECT_OT_SyncObjectProperties(bpy.types.Operator): 371 | """Sync Object Properties""" 372 | bl_idname = "object.sync_object_properties" 373 | bl_label = "Sync object properties" 374 | bl_description = "Synchronizes the properties of an object with another" 375 | bl_options = {'REGISTER'} 376 | 377 | def execute(self, context): 378 | return syncObjectProperties(self, context) 379 | 380 | class OBJECT_OT_RemoveEmptyVertexGroups(bpy.types.Operator): 381 | """Remove Empty Vertex Groups""" 382 | bl_idname = "object.remove_empty_vertex_groups" 383 | bl_label = "Remove empty vertex groups" 384 | bl_description = "Removes empty vertex groups" 385 | bl_options = {'REGISTER'} 386 | 387 | def execute(self, context): 388 | return removeEmptyVertexGroups(self, context) 389 | 390 | class OBJECT_OT_RemoveAllModifiers(bpy.types.Operator): 391 | """Remove All Modifiers""" 392 | bl_idname = "object.remove_all_modifiers" 393 | bl_label = "Remove all modifiers" 394 | bl_description = "Removes all modifiers" 395 | bl_options = {'REGISTER'} 396 | 397 | def execute(self, context): 398 | return removeAllModifiers(self, context) 399 | 400 | class OBJECT_OT_RemoveLocationKeyframes(bpy.types.Operator): 401 | """RemoveLocationKeyframes""" 402 | bl_idname = "object.remove_location_keyframes" 403 | bl_label = "Remove location keyframes" 404 | bl_description = "Removes all recorded location keyframes in selected objects at current frame time" 405 | bl_options = {'REGISTER'} 406 | 407 | def execute(self, context): 408 | return removeKeyframesByChannel(self, context, "location") 409 | 410 | class OBJECT_OT_RemoveEulerRotationKeyframes(bpy.types.Operator): 411 | """RemoveEulerRotationKeyframes""" 412 | bl_idname = "object.remove_euler_rotation_keyframes" 413 | bl_label = "Remove euler rotation keyframes" 414 | bl_description = "Removes all recorded euler rotation keyframes in selected objects at current frame time" 415 | bl_options = {'REGISTER'} 416 | 417 | def execute(self, context): 418 | return removeKeyframesByChannel(self, context, "rotation_euler") 419 | 420 | class OBJECT_OT_RemoveQuatRotationKeyframes(bpy.types.Operator): 421 | """RemoveQuatRotationKeyframes""" 422 | bl_idname = "object.remove_quat_rotation_keyframes" 423 | bl_label = "Remove quat rotation keyframes" 424 | bl_description = "Removes all recorded quat rotation keyframes in selected objects at current frame time" 425 | bl_options = {'REGISTER'} 426 | 427 | def execute(self, context): 428 | return removeKeyframesByChannel(self, context, "rotation_quaternion") 429 | 430 | class OBJECT_OT_RemoveScaleKeyframes(bpy.types.Operator): 431 | """RemoveScaleKeyframes""" 432 | bl_idname = "object.remove_scale_keyframes" 433 | bl_label = "Remove scale keyframes" 434 | bl_description = "Removes all recorded scale keyframes in selected objects at current frame time" 435 | bl_options = {'REGISTER'} 436 | 437 | def execute(self, context): 438 | return removeKeyframesByChannel(self, context, "scale") 439 | 440 | class OBJECT_OT_CleanUpMaterialsAndImages(bpy.types.Operator): 441 | """Clean Up Materials And Images""" 442 | bl_idname = "object.clean_up_materials_and_images" 443 | bl_label = "Clean up materials and images" 444 | bl_description = "Cleans up materials and images" 445 | bl_options = {'REGISTER'} 446 | 447 | def execute(self, context): 448 | return cleanUpMaterialsAndImages(context) 449 | 450 | class SCENE_OT_ToggleRenderers(bpy.types.Operator): 451 | """Toggle Renderers""" 452 | bl_idname = "scene.toggle_renderers" 453 | bl_label = "Toggle renderers" 454 | bl_description = "Toggle renderers (Cycles and Workbench)" 455 | bl_options = {'REGISTER'} 456 | 457 | def execute(self, context): 458 | if context.scene.render.engine == 'CYCLES': 459 | context.scene.render.engine = 'BLENDER_WORKBENCH' 460 | else: 461 | context.scene.render.engine = 'CYCLES' 462 | return {'FINISHED'} 463 | 464 | class SCENE_OT_PauseRender(bpy.types.Operator): 465 | """Pause Render""" 466 | bl_idname = "scene.pause_render" 467 | bl_label = "Pause render" 468 | bl_description = "Pause render in viewport" 469 | bl_options = {'REGISTER'} 470 | 471 | def execute(self, context): 472 | context.scene.cycles.preview_pause = not context.scene.cycles.preview_pause 473 | return {'FINISHED'} 474 | 475 | # ----------------------------------------------------------------------------- 476 | # Panels 477 | 478 | class OBJECT_PT_object_utilities(bpy.types.Panel): 479 | bl_idname = "OBJECT_PT_object_utilities" 480 | bl_label = "Object utilities" 481 | bl_space_type = 'VIEW_3D' 482 | bl_region_type = 'UI' 483 | bl_category = "Edit" 484 | bl_context = 'objectmode' 485 | 486 | @classmethod 487 | def poll(cls, context): 488 | return (context.selected_objects is not None) 489 | 490 | def draw(self, context): 491 | layout = self.layout 492 | box = layout.box() 493 | box.operator("object.diff_object_data") 494 | box.operator("object.sync_object_properties") 495 | box.operator("object.remove_empty_vertex_groups") 496 | box.operator("object.remove_all_modifiers") 497 | box = layout.box() 498 | box.operator("object.remove_location_keyframes") 499 | box.operator("object.remove_euler_rotation_keyframes") 500 | box.operator("object.remove_quat_rotation_keyframes") 501 | box.operator("object.remove_scale_keyframes") 502 | 503 | class OBJECT_PT_misc_utilities(bpy.types.Panel): 504 | bl_idname = "OBJECT_PT_misc_utilities" 505 | bl_label = "Misc utilities" 506 | bl_space_type = 'VIEW_3D' 507 | bl_region_type = 'UI' 508 | bl_category = "Edit" 509 | bl_context = 'objectmode' 510 | 511 | @classmethod 512 | def poll(cls, context): 513 | return True 514 | 515 | def draw(self, context): 516 | layout = self.layout 517 | layout.operator("object.clean_up_materials_and_images") 518 | 519 | class SCENE_PT_render_utilities(bpy.types.Panel): 520 | bl_idname = "SCENE_PT_render_utilities" 521 | bl_label = "Render utilities" 522 | bl_space_type = 'VIEW_3D' 523 | bl_region_type = 'UI' 524 | bl_category = "Edit" 525 | 526 | @classmethod 527 | def poll(cls, context): 528 | return True 529 | 530 | def draw(self, context): 531 | layout = self.layout 532 | layout.operator("scene.toggle_renderers") 533 | layout.operator("scene.pause_render") 534 | 535 | # ----------------------------------------------------------------------------- 536 | # Registering 537 | 538 | addon_keymaps = [] 539 | 540 | def register(): 541 | bpy.utils.register_class(OBJECT_OT_DiffObjectData) 542 | bpy.utils.register_class(OBJECT_OT_SyncObjectProperties) 543 | bpy.utils.register_class(OBJECT_OT_RemoveEmptyVertexGroups) 544 | bpy.utils.register_class(OBJECT_OT_RemoveAllModifiers) 545 | bpy.utils.register_class(OBJECT_OT_RemoveLocationKeyframes) 546 | bpy.utils.register_class(OBJECT_OT_RemoveEulerRotationKeyframes) 547 | bpy.utils.register_class(OBJECT_OT_RemoveQuatRotationKeyframes) 548 | bpy.utils.register_class(OBJECT_OT_RemoveScaleKeyframes) 549 | bpy.utils.register_class(OBJECT_OT_CleanUpMaterialsAndImages) 550 | bpy.utils.register_class(SCENE_OT_ToggleRenderers) 551 | bpy.utils.register_class(SCENE_OT_PauseRender) 552 | bpy.utils.register_class(OBJECT_PT_object_utilities) 553 | bpy.utils.register_class(OBJECT_PT_misc_utilities) 554 | bpy.utils.register_class(SCENE_PT_render_utilities) 555 | 556 | # handle the keymap 557 | wm = bpy.context.window_manager 558 | kc = wm.keyconfigs.addon 559 | if kc: 560 | km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') 561 | kmi = km.keymap_items.new(SCENE_OT_ToggleRenderers.bl_idname, type='R', value='PRESS', alt=True, shift=True) 562 | addon_keymaps.append((km, kmi)) 563 | 564 | def unregister(): 565 | for km, kmi in addon_keymaps: 566 | km.keymap_items.remove(kmi) 567 | addon_keymaps.clear() 568 | 569 | bpy.utils.unregister_class(OBJECT_OT_DiffObjectData) 570 | bpy.utils.unregister_class(OBJECT_OT_SyncObjectProperties) 571 | bpy.utils.unregister_class(OBJECT_OT_RemoveEmptyVertexGroups) 572 | bpy.utils.unregister_class(OBJECT_OT_RemoveAllModifiers) 573 | bpy.utils.unregister_class(OBJECT_OT_RemoveLocationKeyframes) 574 | bpy.utils.unregister_class(OBJECT_OT_RemoveEulerRotationKeyframes) 575 | bpy.utils.unregister_class(OBJECT_OT_RemoveQuatRotationKeyframes) 576 | bpy.utils.unregister_class(OBJECT_OT_RemoveScaleKeyframes) 577 | bpy.utils.unregister_class(OBJECT_OT_CleanUpMaterialsAndImages) 578 | bpy.utils.unregister_class(SCENE_OT_ToggleRenderers) 579 | bpy.utils.unregister_class(SCENE_OT_PauseRender) 580 | bpy.utils.unregister_class(OBJECT_PT_object_utilities) 581 | bpy.utils.unregister_class(OBJECT_PT_misc_utilities) 582 | bpy.utils.unregister_class(SCENE_PT_render_utilities) 583 | 584 | if __name__ == "__main__": 585 | register() 586 | -------------------------------------------------------------------------------- /v3.0/armature_utilities.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Armature utilities", 21 | "author": "Jango73", 22 | "version": (3, 0), 23 | "blender": (3, 0, 0), 24 | "description": "Operations on armatures", 25 | "category": "Object", 26 | } 27 | 28 | import bpy 29 | import mathutils 30 | from bpy.types import Operator 31 | from bpy.props import StringProperty 32 | 33 | # ------------------------------------------------------------------------------------------------- 34 | 35 | def copyArmatureConstraints(self, context): 36 | target = context.selected_objects[0] 37 | 38 | if target is None: 39 | return {'CANCELLED'} 40 | 41 | # get active object 42 | source = context.active_object 43 | 44 | if source is None: 45 | return {'CANCELLED'} 46 | 47 | if target == source: 48 | target = context.selected_objects[1] 49 | 50 | if target is None: 51 | return {'CANCELLED'} 52 | 53 | target.select_set(False) 54 | source.select_set(True) 55 | 56 | bpy.ops.object.posemode_toggle() 57 | bpy.ops.pose.select_all(action='SELECT') 58 | sourceBones = context.selected_pose_bones 59 | bpy.ops.pose.select_all(action='DESELECT') 60 | bpy.ops.object.posemode_toggle() 61 | 62 | for bone in sourceBones: 63 | 64 | bone1 = source.pose.bones[bone.name] 65 | bone2 = target.pose.bones[bone.name] 66 | 67 | if bone2 is not None: 68 | for constraint in bone2.constraints: 69 | bone2.constraints.remove(constraint) 70 | 71 | for constraint in bone1.constraints: 72 | bone2.constraints.copy(constraint) 73 | 74 | for constraint in bone2.constraints: 75 | if hasattr(constraint, 'target'): 76 | if constraint.target == source: 77 | constraint.target = target 78 | 79 | self.report({'INFO'}, "Copied " + source.name + " bone constraints to " + target.name) 80 | 81 | return {'FINISHED'} 82 | 83 | # ------------------------------------------------------------------------------------------------- 84 | # Operators 85 | 86 | class OBJECT_OT_CopyArmatureConstraints(bpy.types.Operator): 87 | """Copy Armature Constraints""" 88 | bl_idname = "object.copy_armature_constraints" 89 | bl_label = "Copy armature constraints" 90 | bl_description = "Copies the bone contraints from an armature to another" 91 | bl_options = {'REGISTER'} 92 | 93 | def execute(self, context): 94 | return copyArmatureConstraints(self, context) 95 | 96 | # ------------------------------------------------------------------------------------------------- 97 | # Save start pose and delta pose 98 | start_pose = {} 99 | pose_delta = {} 100 | 101 | # ------------------------------------------------------------------------------------------------- 102 | 103 | class POSE_OT_MarkStartPose(Operator): 104 | bl_idname = "pose.mark_start_pose" 105 | bl_label = "Mark Start Pose" 106 | bl_description = "Mark the current pose as the start pose" 107 | 108 | @classmethod 109 | def poll(cls, context): 110 | return context.active_object and context.active_object.type == 'ARMATURE' 111 | 112 | def execute(self, context): 113 | global start_pose 114 | armature = context.active_object 115 | pose_bones = armature.pose.bones 116 | 117 | # Clear previous start pose 118 | start_pose.clear() 119 | 120 | # Store current pose (rotation, location, scale) 121 | for bone in pose_bones: 122 | start_pose[bone.name] = { 123 | 'rotation': bone.rotation_quaternion.copy() if bone.rotation_mode == 'QUATERNION' else bone.rotation_euler.to_quaternion(), 124 | 'location': bone.location.copy(), 125 | 'scale': bone.scale.copy() 126 | } 127 | 128 | self.report({'INFO'}, "Marked current pose as start pose") 129 | return {'FINISHED'} 130 | 131 | # ------------------------------------------------------------------------------------------------- 132 | 133 | class POSE_OT_CopyDelta(Operator): 134 | bl_idname = "pose.copy_delta" 135 | bl_label = "Copy Pose Delta" 136 | bl_description = "Copy the delta between marked start pose and current pose" 137 | 138 | @classmethod 139 | def poll(cls, context): 140 | return context.active_object and context.active_object.type == 'ARMATURE' and start_pose 141 | 142 | def execute(self, context): 143 | global pose_delta 144 | armature = context.active_object 145 | pose_bones = armature.pose.bones 146 | 147 | # Clear previous delta 148 | pose_delta.clear() 149 | 150 | # Calculate delta for each bone 151 | for bone in pose_bones: 152 | if bone.name in start_pose: 153 | # Get start and end (current) pose 154 | start_data = start_pose[bone.name] 155 | end_rot = bone.rotation_quaternion.copy() if bone.rotation_mode == 'QUATERNION' else bone.rotation_euler.to_quaternion() 156 | end_loc = bone.location.copy() 157 | end_scale = bone.scale.copy() 158 | 159 | # Calculate delta 160 | delta_rot = end_rot @ start_data['rotation'].inverted() 161 | delta_loc = end_loc - start_data['location'] 162 | delta_scale = mathutils.Vector([end_scale[i] / start_data['scale'][i] if start_data['scale'][i] != 0 else 1.0 for i in range(3)]) 163 | 164 | pose_delta[bone.name] = { 165 | 'rotation': delta_rot, 166 | 'location': delta_loc, 167 | 'scale': delta_scale 168 | } 169 | 170 | self.report({'INFO'}, "Copied delta from start pose to current pose") 171 | return {'FINISHED'} 172 | 173 | # ------------------------------------------------------------------------------------------------- 174 | 175 | class POSE_OT_PasteDelta(Operator): 176 | bl_idname = "pose.paste_delta" 177 | bl_label = "Paste Pose Delta" 178 | bl_description = "Apply the copied pose delta to the current pose" 179 | 180 | @classmethod 181 | def poll(cls, context): 182 | return context.active_object and context.active_object.type == 'ARMATURE' and pose_delta 183 | 184 | def execute(self, context): 185 | armature = context.active_object 186 | pose_bones = armature.pose.bones 187 | 188 | # Apply delta to each bone 189 | for bone in pose_bones: 190 | if bone.name in pose_delta: 191 | delta_data = pose_delta[bone.name] 192 | 193 | # Apply rotation 194 | current_rot = bone.rotation_quaternion.copy() if bone.rotation_mode == 'QUATERNION' else bone.rotation_euler.to_quaternion() 195 | new_rot = delta_data['rotation'] @ current_rot 196 | if bone.rotation_mode == 'QUATERNION': 197 | bone.rotation_quaternion = new_rot 198 | else: 199 | bone.rotation_euler = new_rot.to_euler(bone.rotation_mode) 200 | 201 | # Apply location 202 | bone.location += delta_data['location'] 203 | 204 | # Apply scale 205 | for i in range(3): 206 | bone.scale[i] *= delta_data['scale'][i] 207 | 208 | self.report({'INFO'}, "Pasted pose delta") 209 | return {'FINISHED'} 210 | 211 | # ------------------------------------------------------------------------------------------------- 212 | # Panels 213 | 214 | class OBJECT_PT_armature_utilities(bpy.types.Panel): 215 | bl_idname = "OBJECT_PT_armature_utilities" 216 | bl_label = "Armature Utilities" 217 | bl_space_type = 'VIEW_3D' 218 | bl_region_type = 'UI' 219 | bl_category = "Edit" 220 | bl_context = 'objectmode' 221 | 222 | @classmethod 223 | def poll(cls, context): 224 | return (context.object is not None) 225 | 226 | def draw(self, context): 227 | layout = self.layout 228 | 229 | layout.operator("object.copy_armature_constraints") 230 | 231 | class OBJECT_PT_bone_utilities(bpy.types.Panel): 232 | bl_idname = "OBJECT_PT_bone_utilities" 233 | bl_label = "Bone Utilities" 234 | bl_space_type = 'VIEW_3D' 235 | bl_region_type = 'UI' 236 | bl_category = "Edit" 237 | 238 | @classmethod 239 | def poll(cls, context): 240 | return context.mode == 'POSE' 241 | 242 | def draw(self, context): 243 | layout = self.layout 244 | box = layout.box() 245 | box.operator(POSE_OT_MarkStartPose.bl_idname) 246 | box.operator(POSE_OT_CopyDelta.bl_idname) 247 | box.operator(POSE_OT_PasteDelta.bl_idname) 248 | 249 | # ------------------------------------------------------------------------------------------------- 250 | # Registering 251 | 252 | def register(): 253 | bpy.utils.register_class(OBJECT_OT_CopyArmatureConstraints) 254 | bpy.utils.register_class(OBJECT_PT_armature_utilities) 255 | bpy.utils.register_class(OBJECT_PT_bone_utilities) 256 | bpy.utils.register_class(POSE_OT_MarkStartPose) 257 | bpy.utils.register_class(POSE_OT_CopyDelta) 258 | bpy.utils.register_class(POSE_OT_PasteDelta) 259 | 260 | def unregister(): 261 | bpy.utils.unregister_class(OBJECT_OT_CopyArmatureConstraints) 262 | bpy.utils.unregister_class(OBJECT_PT_armature_utilities) 263 | bpy.utils.unregister_class(OBJECT_PT_bone_utilities) 264 | bpy.utils.unregister_class(POSE_OT_MarkStartPose) 265 | bpy.utils.unregister_class(POSE_OT_CopyDelta) 266 | bpy.utils.unregister_class(POSE_OT_PasteDelta) 267 | 268 | if __name__ == "__main__": 269 | register() 270 | -------------------------------------------------------------------------------- /v3.0/object_utilities.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Object utilities", 21 | "author": "Jango73", 22 | "version": (3, 3), 23 | "blender": (3, 0, 0), 24 | "description": "Operations on objects", 25 | "category": "Object", 26 | } 27 | 28 | import bpy 29 | import re 30 | import copy 31 | 32 | # ------------------------------------------------------------------- 33 | # Global storage for transform copy/paste 34 | _copied_transform = None 35 | 36 | # ----------------------------------------------------------------------------- 37 | # Following two functions are from "blenderartists.org/u/Gorgious" in "object_copy_custom_properties_1_08.py" 38 | 39 | def setProperty(obj, name, value, rna, is_overridable): 40 | if obj is None: 41 | return 42 | 43 | obj[name] = value 44 | obj.id_properties_ensure() 45 | id_properties_ui = obj.id_properties_ui(name) 46 | id_properties_ui.update_from(rna) 47 | obj.property_overridable_library_set(f'["{name}"]', is_overridable) 48 | 49 | def getProperties(obj): 50 | if obj is None: 51 | return tuple() 52 | 53 | obj.id_properties_ensure() 54 | 55 | names = [] 56 | items = obj.items() 57 | rna_properties = { prop.identifier for prop in obj.bl_rna.properties if prop.is_runtime } 58 | 59 | for k, _ in items: 60 | if k in rna_properties: 61 | continue 62 | names.append(k) 63 | 64 | names = list(set(obj.keys()) - set(('cycles_visibility', 'cycles', '_RNA_UI', 'pov'))) 65 | values = [(name, obj[name], obj.id_properties_ui(name), obj.is_property_overridable_library(f'["{name}"]')) for name in names] 66 | 67 | return values 68 | 69 | # ----------------------------------------------------------------------------- 70 | 71 | def showMessageBox(title = "Message Box", icon = 'INFO', lines=""): 72 | myLines=lines 73 | def draw(self, context): 74 | for n in myLines: 75 | self.layout.label(text=n) 76 | bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 77 | 78 | # ----------------------------------------------------------------------------- 79 | 80 | def printToString(targetString, text, no_newline=False): 81 | 82 | temp = targetString.split("\n") 83 | if len(temp) > 0: 84 | if len(temp[-1]) > 200: 85 | targetString += ("\n") 86 | 87 | if (no_newline): 88 | targetString += (text) 89 | else: 90 | targetString += (text + "\n") 91 | 92 | return targetString 93 | 94 | # ----------------------------------------------------------------------------- 95 | 96 | def purgeAll(self, context): 97 | 98 | deleted = 0 99 | bpy.ops.outliner.orphans_purge(num_deleted=deleted, do_recursive=True) 100 | 101 | showMessageBox(lines=["All orphans deleted"]) 102 | 103 | return {'FINISHED'} 104 | 105 | # ----------------------------------------------------------------------------- 106 | 107 | def diffLines(targetString, sourceName, targetName, lines1, lines2): 108 | temp = copy.deepcopy(lines1) 109 | 110 | for x in temp: 111 | if x in lines2: 112 | lines1.remove(x) 113 | lines2.remove(x) 114 | 115 | if len(lines1) == 0 and len(lines2) == 0: 116 | targetString = printToString(targetString, "No difference") 117 | return targetString 118 | 119 | if len(lines1) > 0: 120 | targetString = printToString(targetString, "Only in " + sourceName + ":") 121 | for x in lines1: 122 | targetString = printToString(targetString, x + " ", no_newline=True) 123 | targetString = printToString(targetString, "") 124 | 125 | if len(lines2) > 0: 126 | targetString = printToString(targetString, "Only in " + targetName + ":") 127 | for x in lines2: 128 | targetString = printToString(targetString, x + " ", no_newline=True) 129 | targetString = printToString(targetString, "") 130 | 131 | return targetString 132 | 133 | # ----------------------------------------------------------------------------- 134 | 135 | def diffObjects(self, context): 136 | lineCount = 0 137 | diffCount = 0 138 | sameCount = 0 139 | targetString = "" 140 | lines1 = [] 141 | lines2 = [] 142 | 143 | target = context.selected_objects[0] 144 | 145 | if target is None: 146 | return {'CANCELLED'} 147 | 148 | # get active object 149 | source = context.active_object 150 | 151 | if source is None: 152 | return {'CANCELLED'} 153 | 154 | if target == source: 155 | if len(context.selected_objects) < 2: 156 | return {'CANCELLED'} 157 | target = context.selected_objects[1] 158 | 159 | if target is None: 160 | return {'CANCELLED'} 161 | 162 | targetString = printToString(targetString, "") 163 | targetString = printToString(targetString, "Diff " + source.name + " and " + target.name) 164 | targetString = printToString(targetString, "") 165 | 166 | if len(source.keys()) > 1 and len(target.keys()) > 1: 167 | targetString = printToString(targetString, "[ Custom property names ]") 168 | 169 | for p in source.keys(): 170 | lines1.append(p.strip()) 171 | for p in target.keys(): 172 | lines2.append(p.strip()) 173 | 174 | lines1.sort() 175 | lines2.sort() 176 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 177 | 178 | targetString = printToString(targetString, "[ Custom property values ]") 179 | 180 | for p in source.keys(): 181 | lines1.append("" + p.strip() + "=" + str(source.get(p))) 182 | for p in target.keys(): 183 | lines2.append("" + p.strip() + "=" + str(target.get(p))) 184 | 185 | lines1.sort() 186 | lines2.sort() 187 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 188 | 189 | try: 190 | targetString = printToString(targetString, "") 191 | targetString = printToString(targetString, "[ Vertex groups ]") 192 | 193 | lines1.clear() 194 | lines2.clear() 195 | 196 | for g in source.vertex_groups: 197 | lines1.append(g.name.strip()) 198 | for g in target.vertex_groups: 199 | lines2.append(g.name.strip()) 200 | 201 | lines1.sort() 202 | lines2.sort() 203 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 204 | 205 | except: 206 | pass 207 | 208 | try: 209 | targetString = printToString(targetString, "") 210 | targetString = printToString(targetString, "[ Vertex colors ]") 211 | 212 | lines1.clear() 213 | lines2.clear() 214 | 215 | for v in source.data.vertex_colors.keys(): 216 | lines1.append(v.strip()) 217 | for v in target.data.vertex_colors.keys(): 218 | lines2.append(v.strip()) 219 | 220 | lines1.sort() 221 | lines2.sort() 222 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 223 | 224 | except: 225 | pass 226 | 227 | try: 228 | targetString = printToString(targetString, "") 229 | targetString = printToString(targetString, "[ Modifiers ]") 230 | 231 | lines1.clear() 232 | lines2.clear() 233 | 234 | for m in source.modifiers: 235 | lines1.append(m.name.strip()) 236 | for m in target.modifiers: 237 | lines2.append(m.name.strip()) 238 | 239 | lines1.sort() 240 | lines2.sort() 241 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 242 | 243 | except: 244 | pass 245 | 246 | try: 247 | objectType = getattr(source, 'type', '') 248 | 249 | if objectType in ['ARMATURE']: 250 | targetString = printToString(targetString, "") 251 | targetString = printToString(targetString, "[ Bone constraints ]") 252 | bpy.ops.object.mode_set(mode='POSE') 253 | 254 | lines1.clear() 255 | lines2.clear() 256 | 257 | for b in source.pose.bones: 258 | for c in b.constraints: 259 | lines1.append(b.name.strip() + ":" + c.name.strip()) 260 | for b in target.pose.bones: 261 | for c in b.constraints: 262 | lines2.append(b.name.strip() + ":" + c.name.strip()) 263 | 264 | bpy.ops.object.mode_set(mode='OBJECT') 265 | 266 | lines1.sort() 267 | lines2.sort() 268 | targetString = diffLines(targetString, source.name, target.name, lines1, lines2) 269 | 270 | except: 271 | pass 272 | 273 | showMessageBox(lines=targetString.split("\n")) 274 | 275 | return {'FINISHED'} 276 | 277 | # ----------------------------------------------------------------------------- 278 | 279 | def getMirroredName(name): 280 | if '.R' in name: 281 | return name.replace('.R','.L') 282 | 283 | if '.r' in name: 284 | return name.replace('.r','.l') 285 | 286 | if name.startswith('Right'): 287 | return name.replace('Right', 'Left') 288 | 289 | if name.startswith('right'): 290 | return name.replace('right', 'left') 291 | 292 | if '.L' in name: 293 | return name.replace('.L','.R') 294 | 295 | if '.l' in name: 296 | return name.replace('.l','.r') 297 | 298 | if name.startswith('Left'): 299 | return name.replace('Left', 'Right') 300 | 301 | if name.startswith('left'): 302 | return name.replace('left', 'right') 303 | 304 | return '' 305 | 306 | # ----------------------------------------------------------------------------- 307 | 308 | def syncObjectProperties(self, context): 309 | target = context.selected_objects[0] 310 | source = context.active_object 311 | 312 | if source is None or target is None: 313 | return {'CANCELLED'} 314 | 315 | if target == source: 316 | if len(context.selected_objects) < 2: 317 | return {'CANCELLED'} 318 | target = context.selected_objects[1] 319 | 320 | if target is None: 321 | return {'CANCELLED'} 322 | 323 | values = getProperties(source) 324 | 325 | for value in values: 326 | if value[0] not in target.keys(): 327 | setProperty(target, value[0], value[1], value[2], value[3]) 328 | 329 | self.report({'INFO'}, "Synced " + target.name + " properties with " + source.name) 330 | 331 | return {'FINISHED'} 332 | 333 | # ----------------------------------------------------------------------------- 334 | 335 | def copyMaterialSlots(self, context): 336 | target = context.selected_objects[0] 337 | source = context.active_object 338 | 339 | if source is None or target is None: 340 | return {'CANCELLED'} 341 | 342 | if target == source: 343 | if len(context.selected_objects) < 2: 344 | return {'CANCELLED'} 345 | target = context.selected_objects[1] 346 | 347 | if target is None: 348 | return {'CANCELLED'} 349 | 350 | if len(source.material_slots) > 0: 351 | # Assign the material to each slot 352 | for c, slot in enumerate(source.material_slots): 353 | target.material_slots[c].material = source.material_slots[c].material 354 | 355 | self.report({'INFO'}, "Copied " + source.name + " materials to " + target.name) 356 | 357 | return {'FINISHED'} 358 | 359 | # ----------------------------------------------------------------------------- 360 | 361 | def copyObjectPropertyValues(self, context): 362 | target = context.selected_objects[0] 363 | 364 | if target is None: 365 | return {'CANCELLED'} 366 | 367 | # get active object 368 | source = context.active_object 369 | 370 | if source is None: 371 | return {'CANCELLED'} 372 | 373 | if target == source: 374 | if len(context.selected_objects) < 2: 375 | return {'CANCELLED'} 376 | target = context.selected_objects[1] 377 | 378 | if target is None: 379 | return {'CANCELLED'} 380 | 381 | for p in source.keys(): 382 | if not p.startswith("_"): 383 | if p in target.keys(): 384 | target[p] = source.get(p) 385 | 386 | self.report({'INFO'}, "Copied " + source.name + " property values to " + target.name) 387 | 388 | return {'FINISHED'} 389 | 390 | # ----------------------------------------------------------------------------- 391 | 392 | def makeAllPropertiesOverridable(self, context): 393 | # get active object 394 | obj = context.active_object 395 | 396 | if obj is None: 397 | return {'CANCELLED'} 398 | 399 | for pname in obj.keys(): 400 | try: 401 | qualified_name = "[\"" + pname + "\"]" 402 | obj.property_overridable_library_set(qualified_name, True) 403 | except: 404 | print("Error when processing ", pname) 405 | pass 406 | 407 | return {'FINISHED'} 408 | 409 | # ----------------------------------------------------------------------------- 410 | 411 | def removeEmptyVertexGroups(self, context): 412 | for obj in context.selected_objects: 413 | 414 | obj.update_from_editmode() 415 | 416 | vgroup_used = {i: False for i, k in enumerate(obj.vertex_groups)} 417 | vgroup_names = {i: k.name for i, k in enumerate(obj.vertex_groups)} 418 | vgroup_name_list = list(vgroup_names.values()) 419 | 420 | for v in obj.data.vertices: 421 | for g in v.groups: 422 | 423 | mirrored_name = getMirroredName(vgroup_names[g.group]) 424 | 425 | if mirrored_name in vgroup_name_list: 426 | vgroup_used[g.group] = vgroup_used[vgroup_name_list.index(mirrored_name)] 427 | else: 428 | if g.weight > 0.01: 429 | vgroup_used[g.group] = True 430 | 431 | for i, used in sorted(vgroup_used.items(), reverse=True): 432 | if not used: 433 | obj.vertex_groups.remove(obj.vertex_groups[i]) 434 | 435 | self.report({'INFO'}, "Removed empty groups from " + obj.name) 436 | 437 | return {'FINISHED'} 438 | 439 | # ----------------------------------------------------------------------------- 440 | 441 | def removeAllModifiers(self, context): 442 | for obj in context.selected_objects: 443 | 444 | # remove all modifiers 445 | obj.modifiers.clear() 446 | 447 | self.report({'INFO'}, "Removed all modifiers from " + obj.name) 448 | 449 | return {'FINISHED'} 450 | 451 | # ----------------------------------------------------------------------------- 452 | 453 | def copyObjectTransform(self, context): 454 | global _copied_transform 455 | obj = context.active_object 456 | 457 | if obj is None: 458 | self.report({'ERROR'}, "No active object.") 459 | return {'CANCELLED'} 460 | 461 | _copied_transform = { 462 | "location": obj.location[:], 463 | "rotation": obj.rotation_euler[:], 464 | "scale": obj.scale[:] 465 | } 466 | 467 | self.report({'INFO'}, f"Copied transform: {obj.location[:]}") 468 | return {'FINISHED'} 469 | 470 | # ----------------------------------------------------------------------------- 471 | 472 | def pasteObjectTransform(self, context): 473 | global _copied_transform 474 | obj = context.active_object 475 | 476 | if obj is None: 477 | self.report({'ERROR'}, "No active object.") 478 | return {'CANCELLED'} 479 | 480 | if _copied_transform is None: 481 | self.report({'WARNING'}, "No transform stored. Use 'Copy' first.") 482 | return {'CANCELLED'} 483 | 484 | obj.location = _copied_transform["location"] 485 | obj.rotation_euler = _copied_transform["rotation"] 486 | obj.scale = _copied_transform["scale"] 487 | 488 | self.report({'INFO'}, f"Pasted transform: {obj.location[:]}") 489 | return {'FINISHED'} 490 | 491 | # ----------------------------------------------------------------------------- 492 | 493 | def removeKeyframesByChannel(self, context, channel): 494 | for obj in context.selected_objects: 495 | 496 | if obj.animation_data: 497 | action = obj.animation_data.action 498 | if action: 499 | for fc in action.fcurves: 500 | if fc.data_path.endswith(channel): 501 | try: 502 | obj.keyframe_delete(fc.data_path) 503 | except TypeError: 504 | print(fc.data_path + " channel does not exist. Ignoring.") 505 | 506 | self.report({'INFO'}, "Removed " + channel + " type keyframes from " + obj.name) 507 | 508 | return {'FINISHED'} 509 | 510 | # ----------------------------------------------------------------------------- 511 | 512 | def cleanUpMaterialsAndImages(context): 513 | # iterate over all materials in the file 514 | for material in bpy.data.materials: 515 | 516 | # don't do anything if the material has any users. 517 | if material.users: 518 | continue 519 | 520 | # remove the material otherwise 521 | bpy.data.materials.remove(material) 522 | 523 | # iterate over all images in the file 524 | for image in bpy.data.images: 525 | 526 | # don't do anything if the image has any users. 527 | if image.users: 528 | continue 529 | 530 | # remove the image otherwise 531 | bpy.data.images.remove(image) 532 | 533 | return {'FINISHED'} 534 | 535 | # ----------------------------------------------------------------------------- 536 | # FIX : This creates a mess with the edges of faces 537 | 538 | def rotateFaceVertexIndices(context): 539 | # If in edit mode, switch to object mode and switch back at the end 540 | was_in_edit_mode = False 541 | 542 | for obj in context.selected_objects: 543 | if obj.type == 'MESH': 544 | 545 | if obj.mode == 'EDIT': 546 | bpy.ops.object.mode_set(mode='OBJECT') 547 | was_in_edit_mode = True 548 | 549 | me = obj.data 550 | 551 | for poly in me.polygons: 552 | if not poly.select: 553 | continue 554 | 555 | # Get loops for this poly 556 | # Loop = corner of a poly 557 | loop_start = poly.loop_start 558 | loop_end = loop_start + poly.loop_total 559 | loops = [me.loops[loopi] for loopi in range(loop_start, loop_end)] 560 | 561 | # Get vertex indices for each loop 562 | vidxs = [loop.vertex_index for loop in loops] 563 | 564 | # Shift 565 | vidxs = vidxs[1:] + vidxs[0:1] 566 | 567 | # Write back 568 | for i, loop in enumerate(loops): 569 | loop.vertex_index = vidxs[i] 570 | 571 | # Not sure if you need this 572 | me.update() 573 | 574 | if was_in_edit_mode: 575 | bpy.ops.object.mode_set(mode='EDIT') 576 | 577 | return {'FINISHED'} 578 | 579 | # ----------------------------------------------------------------------------- 580 | # Operators 581 | 582 | class OBJECT_OT_PurgeAll(bpy.types.Operator): 583 | """Purge all""" 584 | bl_idname = "object.purge_all" 585 | bl_label = "Purge all orphans" 586 | bl_description = "Purges all orphan data" 587 | bl_options = {'REGISTER'} 588 | 589 | def execute(self, context): 590 | return purgeAll(self, context) 591 | 592 | class OBJECT_OT_HideAllParticles(bpy.types.Operator): 593 | """Hide all particles""" 594 | bl_idname = "object.hide_all_particles" 595 | bl_label = "Hide all particles" 596 | bl_description = "Hide all particles" 597 | bl_options = {'REGISTER'} 598 | 599 | def execute(self, context): 600 | for object in bpy.data.objects: 601 | if bpy.context.scene in object.users_scene: 602 | for modifier in object.modifiers: 603 | if modifier.type == 'PARTICLE_SYSTEM': 604 | modifier.show_viewport = False 605 | return {'FINISHED'} 606 | 607 | class OBJECT_OT_ShowAllParticles(bpy.types.Operator): 608 | """Show all particles""" 609 | bl_idname = "object.show_all_particles" 610 | bl_label = "Show all particles" 611 | bl_description = "Show all particles" 612 | bl_options = {'REGISTER'} 613 | 614 | def execute(self, context): 615 | for object in bpy.data.objects: 616 | if bpy.context.scene in object.users_scene: 617 | for modifier in object.modifiers: 618 | if modifier.type == 'PARTICLE_SYSTEM': 619 | modifier.show_viewport = True 620 | return {'FINISHED'} 621 | 622 | class OBJECT_OT_DiffObjectData(bpy.types.Operator): 623 | """Diff Object Data""" 624 | bl_idname = "object.diff_object_data" 625 | bl_label = "Diff object data" 626 | bl_description = "Shows difference in object data" 627 | bl_options = {'REGISTER'} 628 | 629 | def execute(self, context): 630 | return diffObjects(self, context) 631 | 632 | class OBJECT_OT_SyncObjectProperties(bpy.types.Operator): 633 | """Sync Object Properties""" 634 | bl_idname = "object.sync_object_properties" 635 | bl_label = "Sync object properties" 636 | bl_description = "Synchronizes the properties of an object with another" 637 | bl_options = {'REGISTER'} 638 | 639 | def execute(self, context): 640 | return syncObjectProperties(self, context) 641 | 642 | class OBJECT_OT_CopyObjectPropertyValues(bpy.types.Operator): 643 | """Copy Object Property Values""" 644 | bl_idname = "object.copy_object_property_values" 645 | bl_label = "Copy object property values" 646 | bl_description = "Copies the custom property values of an object to another" 647 | bl_options = {'REGISTER'} 648 | 649 | def execute(self, context): 650 | return copyObjectPropertyValues(self, context) 651 | 652 | class OBJECT_OT_CopyObjectMaterials(bpy.types.Operator): 653 | """Copy Object Materials""" 654 | bl_idname = "object.copy_object_materials" 655 | bl_label = "Copy object materials" 656 | bl_description = "Copies the materials of an object to another" 657 | bl_options = {'REGISTER'} 658 | 659 | def execute(self, context): 660 | return copyMaterialSlots(self, context) 661 | 662 | class OBJECT_OT_MakeAllPropertiesOverridable(bpy.types.Operator): 663 | """Make All Properties Overridable""" 664 | bl_idname = "object.make_all_properties_overridable" 665 | bl_label = "Make all properties overridable" 666 | bl_description = "Makes all properties overridable" 667 | bl_options = {'REGISTER'} 668 | 669 | def execute(self, context): 670 | return makeAllPropertiesOverridable(self, context) 671 | 672 | class OBJECT_OT_RemoveEmptyVertexGroups(bpy.types.Operator): 673 | """Remove Empty Vertex Groups""" 674 | bl_idname = "object.remove_empty_vertex_groups" 675 | bl_label = "Remove empty vertex groups" 676 | bl_description = "Removes empty vertex groups" 677 | bl_options = {'REGISTER'} 678 | 679 | def execute(self, context): 680 | return removeEmptyVertexGroups(self, context) 681 | 682 | class OBJECT_OT_RemoveAllModifiers(bpy.types.Operator): 683 | """Remove All Modifiers""" 684 | bl_idname = "object.remove_all_modifiers" 685 | bl_label = "Remove all modifiers" 686 | bl_description = "Removes all modifiers" 687 | bl_options = {'REGISTER'} 688 | 689 | def execute(self, context): 690 | return removeAllModifiers(self, context) 691 | 692 | class OBJECT_OT_CopyObjectTransform(bpy.types.Operator): 693 | """Copy Object Transform""" 694 | bl_idname = "object.copy_object_transform" 695 | bl_label = "Copy object transform" 696 | bl_description = "Copies the active object's location, rotation and scale to memory" 697 | bl_options = {'REGISTER'} 698 | 699 | def execute(self, context): 700 | return copyObjectTransform(self, context) 701 | 702 | class OBJECT_OT_PasteObjectTransform(bpy.types.Operator): 703 | """Paste Object Transform""" 704 | bl_idname = "object.paste_object_transform" 705 | bl_label = "Paste object transform" 706 | bl_description = "Pastes location, rotation and scale to the active object from memory" 707 | bl_options = {'REGISTER'} 708 | 709 | def execute(self, context): 710 | return pasteObjectTransform(self, context) 711 | 712 | class OBJECT_OT_RemoveLocationKeyframes(bpy.types.Operator): 713 | """RemoveLocationKeyframes""" 714 | bl_idname = "object.remove_location_keyframes" 715 | bl_label = "Remove location keyframes" 716 | bl_description = "Removes all recorded location keyframes in selected objects at current frame time" 717 | bl_options = {'REGISTER'} 718 | 719 | def execute(self, context): 720 | return removeKeyframesByChannel(self, context, "location") 721 | 722 | class OBJECT_OT_RemoveEulerRotationKeyframes(bpy.types.Operator): 723 | """RemoveEulerRotationKeyframes""" 724 | bl_idname = "object.remove_euler_rotation_keyframes" 725 | bl_label = "Remove euler rotation keyframes" 726 | bl_description = "Removes all recorded euler rotation keyframes in selected objects at current frame time" 727 | bl_options = {'REGISTER'} 728 | 729 | def execute(self, context): 730 | return removeKeyframesByChannel(self, context, "rotation_euler") 731 | 732 | class OBJECT_OT_RemoveQuatRotationKeyframes(bpy.types.Operator): 733 | """RemoveQuatRotationKeyframes""" 734 | bl_idname = "object.remove_quat_rotation_keyframes" 735 | bl_label = "Remove quat rotation keyframes" 736 | bl_description = "Removes all recorded quat rotation keyframes in selected objects at current frame time" 737 | bl_options = {'REGISTER'} 738 | 739 | def execute(self, context): 740 | return removeKeyframesByChannel(self, context, "rotation_quaternion") 741 | 742 | class OBJECT_OT_RemoveScaleKeyframes(bpy.types.Operator): 743 | """RemoveScaleKeyframes""" 744 | bl_idname = "object.remove_scale_keyframes" 745 | bl_label = "Remove scale keyframes" 746 | bl_description = "Removes all recorded scale keyframes in selected objects at current frame time" 747 | bl_options = {'REGISTER'} 748 | 749 | def execute(self, context): 750 | return removeKeyframesByChannel(self, context, "scale") 751 | 752 | class OBJECT_OT_CleanUpMaterialsAndImages(bpy.types.Operator): 753 | """Clean Up Materials And Images""" 754 | bl_idname = "object.clean_up_materials_and_images" 755 | bl_label = "Clean up materials and images" 756 | bl_description = "Cleans up materials and images" 757 | bl_options = {'REGISTER'} 758 | 759 | def execute(self, context): 760 | return cleanUpMaterialsAndImages(context) 761 | 762 | class OBJECT_OT_RotateFaceVertexIndices(bpy.types.Operator): 763 | """Rotate selected faces' vertex indices""" 764 | bl_idname = "object.rotate_face_vertex_indices" 765 | bl_label = "Rotates selected faces' vertex indices" 766 | bl_description = "Rotates selected faces' vertex indices" 767 | bl_options = {'REGISTER'} 768 | 769 | def execute(self, context): 770 | return rotateFaceVertexIndices(context) 771 | 772 | class SCENE_OT_ToggleRenderers(bpy.types.Operator): 773 | """Toggle Renderers""" 774 | bl_idname = "scene.toggle_renderers" 775 | bl_label = "Toggle renderers" 776 | bl_description = "Toggle renderers (Cycles and Workbench)" 777 | bl_options = {'REGISTER'} 778 | 779 | def execute(self, context): 780 | if context.scene.render.engine == 'CYCLES': 781 | context.scene.render.engine = 'BLENDER_WORKBENCH' 782 | else: 783 | context.scene.render.engine = 'CYCLES' 784 | return {'FINISHED'} 785 | 786 | class SCENE_OT_PauseRender(bpy.types.Operator): 787 | """Pause Render""" 788 | bl_idname = "scene.pause_render" 789 | bl_label = "Pause render" 790 | bl_description = "Pause render in viewport" 791 | bl_options = {'REGISTER'} 792 | 793 | def execute(self, context): 794 | context.scene.cycles.preview_pause = not context.scene.cycles.preview_pause 795 | return {'FINISHED'} 796 | 797 | # ----------------------------------------------------------------------------- 798 | # Panels 799 | 800 | class OBJECT_PT_object_utilities(bpy.types.Panel): 801 | bl_idname = "OBJECT_PT_object_utilities" 802 | bl_label = "Object utilities" 803 | bl_space_type = 'VIEW_3D' 804 | bl_region_type = 'UI' 805 | bl_category = "Edit" 806 | bl_context = 'objectmode' 807 | 808 | @classmethod 809 | def poll(cls, context): 810 | return (context.selected_objects is not None) 811 | 812 | def draw(self, context): 813 | layout = self.layout 814 | 815 | box = layout.box() 816 | box.operator("object.purge_all") 817 | box.operator("object.hide_all_particles") 818 | box.operator("object.show_all_particles") 819 | 820 | box = layout.box() 821 | box.operator("object.diff_object_data") 822 | box.operator("object.sync_object_properties") 823 | box.operator("object.copy_object_property_values") 824 | box.operator("object.copy_object_materials") 825 | box.operator("object.make_all_properties_overridable") 826 | box.operator("object.remove_empty_vertex_groups") 827 | box.operator("object.remove_all_modifiers") 828 | 829 | box = layout.box() 830 | box.operator("object.copy_object_transform") 831 | box.operator("object.paste_object_transform") 832 | 833 | box = layout.box() 834 | box.operator("object.remove_location_keyframes") 835 | box.operator("object.remove_euler_rotation_keyframes") 836 | box.operator("object.remove_quat_rotation_keyframes") 837 | box.operator("object.remove_scale_keyframes") 838 | 839 | class OBJECT_PT_misc_utilities(bpy.types.Panel): 840 | bl_idname = "OBJECT_PT_misc_utilities" 841 | bl_label = "Misc utilities" 842 | bl_space_type = 'VIEW_3D' 843 | bl_region_type = 'UI' 844 | bl_category = "Edit" 845 | bl_context = 'objectmode' 846 | 847 | @classmethod 848 | def poll(cls, context): 849 | return True 850 | 851 | def draw(self, context): 852 | layout = self.layout 853 | layout.operator("object.clean_up_materials_and_images") 854 | 855 | class OBJECT_PT_object_edit_utilities(bpy.types.Panel): 856 | bl_idname = "OBJECT_PT_object_edit_utilities" 857 | bl_label = "Object edit utilities" 858 | bl_space_type = 'VIEW_3D' 859 | bl_region_type = 'UI' 860 | bl_category = "Edit" 861 | bl_context = 'mesh_edit' 862 | 863 | @classmethod 864 | def poll(cls, context): 865 | return True 866 | # (context.selected_objects is not None) 867 | 868 | def draw(self, context): 869 | layout = self.layout 870 | box = layout.box() 871 | box.operator("object.rotate_face_vertex_indices") 872 | 873 | class SCENE_PT_render_utilities(bpy.types.Panel): 874 | bl_idname = "SCENE_PT_render_utilities" 875 | bl_label = "Render utilities" 876 | bl_space_type = 'VIEW_3D' 877 | bl_region_type = 'UI' 878 | bl_category = "Edit" 879 | 880 | @classmethod 881 | def poll(cls, context): 882 | return True 883 | 884 | def draw(self, context): 885 | layout = self.layout 886 | layout.operator("scene.toggle_renderers") 887 | layout.operator("scene.pause_render") 888 | 889 | # ----------------------------------------------------------------------------- 890 | # Registering 891 | 892 | addon_keymaps = [] 893 | 894 | def register(): 895 | bpy.utils.register_class(OBJECT_OT_PurgeAll) 896 | bpy.utils.register_class(OBJECT_OT_HideAllParticles) 897 | bpy.utils.register_class(OBJECT_OT_ShowAllParticles) 898 | bpy.utils.register_class(OBJECT_OT_DiffObjectData) 899 | bpy.utils.register_class(OBJECT_OT_SyncObjectProperties) 900 | bpy.utils.register_class(OBJECT_OT_CopyObjectPropertyValues) 901 | bpy.utils.register_class(OBJECT_OT_CopyObjectMaterials) 902 | bpy.utils.register_class(OBJECT_OT_MakeAllPropertiesOverridable) 903 | bpy.utils.register_class(OBJECT_OT_RemoveEmptyVertexGroups) 904 | bpy.utils.register_class(OBJECT_OT_RemoveAllModifiers) 905 | 906 | bpy.utils.register_class(OBJECT_OT_CopyObjectTransform) 907 | bpy.utils.register_class(OBJECT_OT_PasteObjectTransform) 908 | bpy.utils.register_class(OBJECT_OT_RemoveLocationKeyframes) 909 | bpy.utils.register_class(OBJECT_OT_RemoveEulerRotationKeyframes) 910 | bpy.utils.register_class(OBJECT_OT_RemoveQuatRotationKeyframes) 911 | bpy.utils.register_class(OBJECT_OT_RemoveScaleKeyframes) 912 | 913 | bpy.utils.register_class(OBJECT_OT_CleanUpMaterialsAndImages) 914 | # bpy.utils.register_class(OBJECT_OT_RotateFaceVertexIndices) 915 | bpy.utils.register_class(SCENE_OT_ToggleRenderers) 916 | bpy.utils.register_class(SCENE_OT_PauseRender) 917 | 918 | bpy.utils.register_class(OBJECT_PT_object_utilities) 919 | bpy.utils.register_class(OBJECT_PT_misc_utilities) 920 | # bpy.utils.register_class(OBJECT_PT_object_edit_utilities) 921 | bpy.utils.register_class(SCENE_PT_render_utilities) 922 | 923 | # handle the keymap 924 | wm = bpy.context.window_manager 925 | kc = wm.keyconfigs.addon 926 | if kc: 927 | km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D') 928 | kmi = km.keymap_items.new(SCENE_OT_ToggleRenderers.bl_idname, type='R', value='PRESS', alt=True, shift=True) 929 | addon_keymaps.append((km, kmi)) 930 | 931 | def unregister(): 932 | for km, kmi in addon_keymaps: 933 | km.keymap_items.remove(kmi) 934 | addon_keymaps.clear() 935 | 936 | bpy.utils.unregister_class(OBJECT_OT_PurgeAll) 937 | bpy.utils.unregister_class(OBJECT_OT_HideAllParticles) 938 | bpy.utils.unregister_class(OBJECT_OT_ShowAllParticles) 939 | bpy.utils.unregister_class(OBJECT_OT_DiffObjectData) 940 | bpy.utils.unregister_class(OBJECT_OT_SyncObjectProperties) 941 | bpy.utils.unregister_class(OBJECT_OT_CopyObjectPropertyValues) 942 | bpy.utils.unregister_class(OBJECT_OT_CopyObjectMaterials) 943 | bpy.utils.unregister_class(OBJECT_OT_MakeAllPropertiesOverridable) 944 | bpy.utils.unregister_class(OBJECT_OT_RemoveEmptyVertexGroups) 945 | bpy.utils.unregister_class(OBJECT_OT_RemoveAllModifiers) 946 | 947 | bpy.utils.unregister_class(OBJECT_OT_CopyObjectTransform) 948 | bpy.utils.unregister_class(OBJECT_OT_PasteObjectTransform) 949 | bpy.utils.unregister_class(OBJECT_OT_RemoveLocationKeyframes) 950 | bpy.utils.unregister_class(OBJECT_OT_RemoveEulerRotationKeyframes) 951 | bpy.utils.unregister_class(OBJECT_OT_RemoveQuatRotationKeyframes) 952 | bpy.utils.unregister_class(OBJECT_OT_RemoveScaleKeyframes) 953 | 954 | bpy.utils.unregister_class(OBJECT_OT_CleanUpMaterialsAndImages) 955 | # bpy.utils.unregister_class(OBJECT_OT_RotateFaceVertexIndices) 956 | bpy.utils.unregister_class(SCENE_OT_ToggleRenderers) 957 | bpy.utils.unregister_class(SCENE_OT_PauseRender) 958 | 959 | bpy.utils.unregister_class(OBJECT_PT_object_utilities) 960 | bpy.utils.unregister_class(OBJECT_PT_misc_utilities) 961 | # bpy.utils.unregister_class(OBJECT_PT_object_edit_utilities) 962 | bpy.utils.unregister_class(SCENE_PT_render_utilities) 963 | 964 | if __name__ == "__main__": 965 | register() 966 | --------------------------------------------------------------------------------