├── MultiEdit_05.py ├── MultiEdit_1_0.py └── MultiEdit_1_1.py /MultiEdit_05.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": "MultiEdit", 21 | "author": "Antonis Karvelas", 22 | "version": (0, 5), 23 | "blender": (2, 7, 8), 24 | "location": "VIEW 3D > Tools > Multiple Objects Editing ", 25 | "description": "Allows you to edit multiple objects together in edit mode without destroying data", 26 | "warning": "Alpha Version 0.5, maybe has a few problems...", 27 | "wiki_url": "", 28 | "tracker_url": "", 29 | "category": "Mesh"} 30 | 31 | #Imports: 32 | import bpy 33 | import math 34 | 35 | #Create a list to put the names of the selected objects: 36 | name_list = [] 37 | 38 | #Create a list to put the names of the duplicated objects: 39 | duplicated_list = [] 40 | 41 | #Create a list to put the vertex_groups that need to be maintained: 42 | special_vgroups_list = [] 43 | 44 | #Create a dictionary to put the parents 45 | parents_list = {} 46 | 47 | class MultiEdit_Enter(bpy.types.Operator): 48 | bl_label = "MultiEdit Enter" 49 | bl_idname = "objects.multiedit_enter_operator" 50 | 51 | #The main function of the class: 52 | def execute(self, context): 53 | #Create a list with all the selected objects: 54 | selected_objects_list = bpy.context.selected_objects 55 | 56 | #Deselect all the non-mesh objects: 57 | for visible_object in selected_objects_list: 58 | if visible_object.type != "MESH": 59 | visible_object.select = False 60 | else: 61 | pass 62 | 63 | #Create a new list with all the selected objects: 64 | new_selected_objects_list = bpy.context.selected_objects 65 | 66 | #If there's only one selected object, enter Edit-mode, else create 67 | #a MultiEdit instance: 68 | if len(new_selected_objects_list) >= 2: 69 | 70 | #If the name_list contains elements, that means that there is 71 | #at least one more MultiEdit instance. 72 | if len(name_list) == 0: 73 | self.Create_MultiEdit(new_selected_objects_list) 74 | else: 75 | raise ValueError("A MultiEdit instance is already running!") 76 | else: 77 | bpy.ops.object.mode_set(mode = 'EDIT') 78 | return {'FINISHED'} 79 | 80 | #The function that actually initiated a new MultiEdit: 81 | def Create_MultiEdit(self, objects): 82 | 83 | #Create a variable to keep track of the number of objects: 84 | copied_index = 0 85 | 86 | #Iterate through the given objects list: 87 | for object in objects: 88 | 89 | #Append the object's name to the name_list: 90 | name_list.append(object.name) 91 | 92 | #Call the Duplicate_Object function: 93 | new_object_name = object.name + "_dupl" + str(copied_index) 94 | self.Duplicate_Object(bpy.context.scene, (new_object_name),object) 95 | duplicated_list.append(new_object_name) 96 | 97 | #Increase the copied_index by 1: 98 | copied_index += 1 99 | 100 | #Call the Create_Vertex_Groups function: 101 | self.Create_Vertex_Groups(object) 102 | 103 | #Move duplicated objects to diferent layer: 104 | bpy.ops.object.select_all(action = 'DESELECT') 105 | bpy.data.objects[new_object_name].select = True 106 | layers = [] 107 | for i in object.layers: 108 | layers.append(i) 109 | bpy.ops.object.move_to_layer(layers = layers) 110 | bpy.ops.object.hide_view_set() 111 | ##bpy.ops.object.move_to_layer(layers=((False,)*19 +(True,)))## 112 | 113 | #Call the Remove_Modifiers_and_Constraints function: 114 | self.Remove_Modifiers_and_Constraints(object) 115 | 116 | #Finally, join all the objects and enter Edit-mode: 117 | for object in objects: 118 | bpy.data.objects[object.name].select = True 119 | bpy.context.scene.objects.active = objects[0] 120 | bpy.ops.object.join() 121 | bpy.context.active_object.name = "MultiEdit" 122 | bpy.ops.object.mode_set(mode = 'EDIT') 123 | 124 | 125 | def Create_Vertex_Groups(self, object): 126 | 127 | ###Create the necessary vertex groups:### 128 | bpy.context.scene.objects.active = object 129 | for vertex_group in object.vertex_groups: 130 | special_vgroups_list.append(vertex_group.name) 131 | 132 | #Create vertex groups containting all the vertices: 133 | object.vertex_groups.new(object.name) 134 | vertex_group = object.vertex_groups[-1] 135 | verts = [vert.index for vert in object.data.vertices] 136 | vertex_group.add(verts, 1.0, 'ADD') 137 | 138 | #####for vert in object.data.vertices: 139 | ##### verts.append(vert.index) 140 | ##### vertex_group.add(verts, 1.0, 'ADD') 141 | 142 | 143 | def Remove_Modifiers_and_Constraints(self, object): 144 | 145 | ###Remove all the modifiers and constraints from the objects:### 146 | object.select = True 147 | for modifier in object.modifiers: 148 | object.modifiers.remove(modifier) 149 | for constraint in object.constraints: 150 | object.constraints.remove(constraint) 151 | 152 | def Duplicate_Object(self, scene, name, old_object): 153 | 154 | ###Duplicate the given object:### 155 | 156 | #Create new mesh: 157 | mesh = bpy.data.meshes.new(name) 158 | 159 | #Create new object associated with the mesh: 160 | ob_new = bpy.data.objects.new(name, mesh) 161 | 162 | #Copy data block from the old object into the new object: 163 | ob_new.data = old_object.data.copy() 164 | ob_new.scale = old_object.scale 165 | ob_new.rotation_euler = old_object.rotation_euler 166 | ob_new.location = old_object.location 167 | 168 | #Link new object to the given scene and select it 169 | scene.objects.link(ob_new) 170 | 171 | #Copy all the modifiers: 172 | for mod in old_object.modifiers: 173 | mod_new = ob_new.modifiers.new(mod.name, mod.type) 174 | properties = [p.identifier for p in mod.bl_rna.properties 175 | if not p.is_readonly] 176 | for prop in properties: 177 | setattr(mod_new, prop, getattr(mod, prop)) 178 | 179 | #Copy all the constraints: 180 | for constr in old_object.constraints: 181 | constr_new = ob_new.constraints.new(constr.type) 182 | properties = [p.identifier for p in constr.bl_rna.properties 183 | if not p.is_readonly] 184 | for prop in properties: 185 | setattr(constr_new, prop, getattr(constr, prop)) 186 | 187 | #Copy all the vertex groups: 188 | for vertex_g in old_object.vertex_groups: 189 | vert_g_new = ob_new.vertex_groups.new(vertex_g.name) 190 | properties = [p.identifier for p in vertex_g.bl_rna.properties 191 | if not p.is_readonly] 192 | for prop in properties: 193 | setattr(vert_g_new, prop, getattr(vertex_g, prop)) 194 | 195 | #Copy all the object groups: 196 | for group in old_object.users_group: 197 | bpy.context.scene.objects.active = ob_new 198 | bpy.ops.object.group_link(group=group.name) 199 | 200 | #Copy parent object value 201 | try: 202 | parents_list[old_object.name] = (old_object.parent).name 203 | except: 204 | pass 205 | #Finally, return the new object: 206 | return ob_new 207 | 208 | 209 | class MultiEdit_Exit(bpy.types.Operator): 210 | bl_label = "MultiEdit Exit" 211 | bl_idname = "objects.multiedit_exit_operator" 212 | bl_context = "editmode" 213 | 214 | #The main function of the class: 215 | def execute(self,context): 216 | 217 | #In case that any problems arise, just cancel the MultiEdit: 218 | try: 219 | bpy.context.scene.objects.active = bpy.data.objects["MultiEdit"] 220 | except: 221 | del name_list[:] 222 | del duplicated_list[:] 223 | del special_vgroups_list[:] 224 | parents_list.clear() 225 | 226 | #Create the necessary variables: 227 | active_object = bpy.context.active_object 228 | name = active_object.name 229 | vgroup_index = 0 230 | 231 | self.Separate_Objects(active_object, name, vgroup_index) 232 | self.Fix_Objects(active_object, name, vgroup_index) 233 | 234 | return {'FINISHED'} 235 | 236 | def Separate_Objects(self, active_object, name, vgroup_index): 237 | 238 | ###Separate given object according to the vertex_groups: 239 | for vertex_group in active_object.vertex_groups: 240 | bpy.ops.object.mode_set(mode = 'EDIT') 241 | bpy.ops.mesh.select_all(action = 'DESELECT') 242 | bpy.ops.mesh.select_mode(type="VERT") 243 | bpy.ops.object.mode_set(mode = 'OBJECT') 244 | 245 | for vert in active_object.data.vertices: 246 | for vertGroup in vert.groups: 247 | if vertGroup.group == vgroup_index: 248 | if vertex_group.name in special_vgroups_list: 249 | break 250 | else: 251 | vert.select = True 252 | else: 253 | pass 254 | 255 | 256 | 257 | bpy.ops.object.mode_set(mode = 'EDIT') 258 | try: 259 | bpy.ops.mesh.separate(type="SELECTED") 260 | except: 261 | pass 262 | vgroup_index += 1 263 | bpy.ops.object.mode_set(mode = 'OBJECT') 264 | 265 | 266 | def Fix_Objects(self, active_object, name, vgroup_index): 267 | 268 | existing_vg = [] 269 | object_layer = bpy.context.scene.active_layer 270 | for object in bpy.context.selected_objects: 271 | bpy.context.scene.objects.active = object 272 | del existing_vg[:] 273 | vgroup_index = 0 274 | for vg in object.vertex_groups: 275 | bpy.ops.object.mode_set(mode = 'EDIT') 276 | bpy.ops.mesh.select_all(action = 'DESELECT') 277 | bpy.ops.object.mode_set(mode = 'OBJECT') 278 | #Finding which vertex group has verts 279 | for vert in object.data.vertices: 280 | for vertGroup in vert.groups: 281 | if vertGroup.group == vgroup_index: 282 | vert.select = True 283 | if object.vertex_groups[vgroup_index].name in special_vgroups_list: 284 | pass 285 | elif object.vertex_groups[vgroup_index].name in existing_vg: 286 | pass 287 | else: 288 | existing_vg.append(object.vertex_groups[vgroup_index].name) 289 | 290 | vgroup_index += 1 291 | 292 | #RENAME OBJECTS, ORGANIZES MATERIALS 293 | if len(existing_vg) < 2 and len(existing_vg) > 0: 294 | # try: 295 | object.name = existing_vg[0] 296 | wanted_object_name = duplicated_list[(name_list.index(object.name))] 297 | 298 | mats = [] 299 | 300 | for slot in object.material_slots: 301 | for slot_dupl in bpy.data.objects[wanted_object_name].material_slots: 302 | if slot_dupl.name == slot.name: 303 | mats.append(slot_dupl.name) 304 | else: 305 | pass 306 | 307 | var = 0 308 | for mat_index in range(len(object.material_slots)): 309 | try: 310 | if object.material_slots[mat_index - var].name in mats: 311 | pass 312 | else: 313 | bpy.context.scene.objects.active = object 314 | #mat_index = object.material_slots[slot_dupl].index 315 | object.active_material_index = mat_index - var 316 | bpy.ops.object.material_slot_remove() 317 | var +=1 318 | except: 319 | pass 320 | 321 | #Call the Copy_Data function: 322 | self.Copy_Data(wanted_object_name, object) 323 | 324 | else: 325 | object.name = "New Geometry" 326 | 327 | #Call the Delete_Objects function: 328 | self.Delete_Objects() 329 | 330 | #Call the Preserve_Data function: 331 | self.Preserve_Data() 332 | 333 | #Unhide all objects: 334 | bpy.ops.object.hide_view_clear() 335 | 336 | #Delete duplicated objects 337 | bpy.ops.object.select_all(action="SELECT") 338 | for obj in bpy.context.selected_objects: 339 | if "dupl" not in obj.name: 340 | obj.select = False 341 | bpy.ops.object.delete() 342 | 343 | #Empty lists for future use 344 | del name_list[:] 345 | del duplicated_list[:] 346 | del special_vgroups_list[:] 347 | parents_list.clear() 348 | 349 | def Copy_Data(self, wanted_object_name, object): 350 | 351 | ###All those things copy modifiers, constraints etc. with all properties. Cool...### 352 | bpy.ops.object.select_all(action = 'DESELECT') 353 | object.select = True 354 | layers = [] 355 | for i in bpy.data.objects[wanted_object_name].layers: 356 | layers.append(i) 357 | bpy.ops.object.move_to_layer(layers = layers) 358 | 359 | #Copy modifiers: 360 | properties = [] 361 | for mod in bpy.data.objects[wanted_object_name].modifiers: 362 | mod_new = object.modifiers.new(mod.name, mod.type) 363 | properties = [p.identifier for p in mod.bl_rna.properties 364 | if not p.is_readonly] 365 | for prop in properties: 366 | setattr(mod_new, prop, getattr(mod, prop)) 367 | 368 | #Remove all existing groups: 369 | bpy.context.scene.objects.active = object 370 | bpy.ops.group.objects_remove_all() 371 | 372 | #Copy object groups: 373 | for group in bpy.data.objects[wanted_object_name].users_group: 374 | bpy.context.scene.objects.active = object 375 | bpy.ops.object.group_link(group=group.name) 376 | 377 | #Delete unnecessary vertex groups: 378 | for vg in object.vertex_groups: 379 | if vg.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].vertex_groups: 380 | pass 381 | else: 382 | object.vertex_groups.remove(vg) 383 | 384 | #Copy constraints: 385 | for constr in bpy.data.objects[wanted_object_name].constraints: 386 | constr_new = object.constraints.new(constr.type) 387 | properties = [p.identifier for p in constr.bl_rna.properties 388 | if not p.is_readonly] 389 | for prop in properties: 390 | setattr(constr_new, prop, getattr(constr, prop)) 391 | 392 | #Copy shape keys: 393 | try: 394 | for shape_key in object.data.shape_keys.key_blocks: 395 | try: 396 | bpy.context.scene.objects.active = object 397 | if shape_key.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].data.shape_keys.key_blocks: 398 | pass 399 | else: 400 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 401 | object.active_shape_key_index = idx 402 | bpy.ops.object.shape_key_remove() 403 | except: 404 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 405 | object.active_shape_key_index = idx 406 | bpy.ops.object.shape_key_remove() 407 | except: 408 | pass 409 | 410 | def Preserve_Data(self): 411 | #Check if checkbox is true and preserve or not the rotation/scale values of the objects: 412 | if bpy.context.scene.Preserve_Location_Rotation_Scale: 413 | for obj in bpy.data.objects: 414 | self.Preserve_Parents(obj) 415 | for nam in name_list: 416 | if nam == obj.name: 417 | obj.select = True 418 | bpy.context.scene.objects.active = obj 419 | 420 | #Location: 421 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 422 | #Rotation: 423 | rotation_values = bpy.data.objects[duplicated_list[name_list.index(nam)]].rotation_euler 424 | 425 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 426 | 427 | rot = (rotation_values[0]*(-1), 428 | rotation_values[1]*(-1), 429 | rotation_values[2]*(-1)) 430 | 431 | obj.rotation_mode = 'ZYX' 432 | obj.rotation_euler = (rot) 433 | 434 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 435 | 436 | rot = (rotation_values[0], 437 | rotation_values[1], 438 | rotation_values[2]) 439 | 440 | obj.rotation_mode = 'XYZ' 441 | obj.rotation_euler = (rot) 442 | 443 | #Scale/Dimensions: 444 | scales = bpy.data.objects[duplicated_list[name_list.index(nam)]].scale 445 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 446 | bpy.ops.object.mode_set(mode = 'EDIT') 447 | bpy.ops.mesh.select_all(action = 'SELECT') 448 | bpy.ops.transform.resize(value=(1.0/scales[0], 1.0/scales[1], 1.0/scales[2])) 449 | bpy.ops.object.mode_set(mode = 'OBJECT') 450 | obj.scale = scales 451 | bpy.ops.object.mode_set(mode = 'EDIT') 452 | bpy.ops.object.mode_set(mode = 'OBJECT') 453 | 454 | bpy.context.scene.cursor_location = bpy.data.objects[duplicated_list[name_list.index(nam)]].location 455 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR") 456 | 457 | #Deselect object: 458 | obj.select = False 459 | 460 | else: 461 | pass 462 | 463 | else: 464 | for obj in bpy.data.objects: 465 | for nam in name_list: 466 | self.Preserve_Parents(obj) 467 | if nam in obj.name: 468 | obj.select = True 469 | 470 | #Location: 471 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 472 | 473 | #Rotation/Scale: 474 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 475 | 476 | else: 477 | pass 478 | 479 | #A function to preserve an object's parent: 480 | def Preserve_Parents(self, obj): 481 | try: 482 | obj.parent = bpy.data.objects[parents_list[obj.name]] 483 | except: 484 | pass 485 | 486 | #Quite simple. Deletes the objects that don't have any geometry 487 | def Delete_Objects(self): 488 | 489 | bpy.ops.object.select_all(action="DESELECT") 490 | try: 491 | bpy.data.objects["New Geometry"].select = True 492 | vert_check = bpy.data.objects["New Geometry"] 493 | if len(vert_check.data.vertices) > 0: 494 | self.Clear_New_Geometry_Data("New Geometry") 495 | else: 496 | bpy.ops.object.delete() 497 | except: 498 | pass 499 | 500 | bpy.ops.object.select_all(action="DESELECT") 501 | try: 502 | bpy.data.objects["MultiEdit"].select = True 503 | vert_check = bpy.data.objects["MultiEdit"] 504 | if len(vert_check.data.vertices) > 0: 505 | bpy.data.objects["MultiEdit"].name = "New Geometry" 506 | self.Clear_New_Geometry_Data("New Geometry") 507 | else: 508 | bpy.ops.object.delete() 509 | except: 510 | pass 511 | 512 | #Clears all the new geometry's data. 513 | def Clear_New_Geometry_Data(self, obj_name): 514 | main_object = bpy.data.objects[obj_name] 515 | 516 | #Remove object groups: 517 | bpy.context.scene.objects.active = main_object 518 | bpy.ops.group.objects_remove_all() 519 | 520 | #Remove vertex groups: 521 | for vgroup in main_object.vertex_groups: 522 | main_object.vertex_groups.remove(vgroup) 523 | 524 | #Remove UV layers: 525 | for uv in main_object.data.uv_textures: 526 | main_object.data.uv_textures.remove(uv) 527 | 528 | #Set origin point to center of geometry: 529 | main_object.select = True 530 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 531 | 532 | #Remove materials: 533 | while main_object.data.materials: 534 | main_object.data.materials.pop() 535 | 536 | 537 | 538 | class MultiEdit_Panel(bpy.types.Panel): 539 | bl_label = "Multiple Objects Editing" 540 | bl_idname = "MultiEdit" 541 | bl_space_type = "VIEW_3D" 542 | bl_region_type = "TOOLS" 543 | bl_category = "Tools" 544 | 545 | def draw(self, context): 546 | layout = self.layout 547 | sce = bpy.context.scene 548 | 549 | layout.operator(MultiEdit_Enter.bl_idname) 550 | layout.operator(MultiEdit_Exit.bl_idname) 551 | layout.prop(sce, "Preserve_Location_Rotation_Scale") 552 | 553 | def register(): 554 | bpy.utils.register_class(MultiEdit_Enter) 555 | bpy.utils.register_class(MultiEdit_Exit) 556 | bpy.utils.register_class(MultiEdit_Panel) 557 | bpy.types.Scene.Preserve_Location_Rotation_Scale = bpy.props.BoolProperty \ 558 | ( 559 | name = "Preserve Location/Rotation/Scale", 560 | description = "Preserve the Location/Rotation/Scale values of the objects.", 561 | default = True 562 | ) 563 | 564 | def unregister(): 565 | bpy.utils.unregister_class(MultiEdit_Enter); 566 | bpy.utils.unregister_class(MultiEdit_Exit); 567 | bpy.utils.unregister_class(MultiEdit_Panel); 568 | 569 | if __name__ == "__main__": 570 | register() 571 | -------------------------------------------------------------------------------- /MultiEdit_1_0.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": "MultiEdit", 21 | "author": "Antonis Karvelas, Dimitris Chloupis", 22 | "version": (1, 0), 23 | "blender": (2, 7, 8), 24 | "location": "VIEW 3D > Tools > Multiple Objects Editing ", 25 | "description": "Allows you to edit multiple objects together in edit mode without destroying data.", 26 | "warning": "Could still have a few issues, be careful.", 27 | "wiki_url": "", 28 | "tracker_url": "", 29 | "category": "Mesh"} 30 | 31 | #Imports: 32 | import bpy 33 | import math 34 | from mathutils import Vector 35 | from bpy.props import FloatVectorProperty 36 | import time 37 | 38 | #Create a list to put the names of the selected objects: 39 | name_list = [] 40 | 41 | #Create a list to put the names of the duplicated objects: 42 | duplicated_list = [] 43 | 44 | #Create a list to put the vertex_groups that need to be maintained: 45 | special_vgroups_list = [] 46 | 47 | #Create a dictionary to put the parents 48 | parents_list = {} 49 | 50 | 51 | class MultiEdit_Enter(bpy.types.Operator): 52 | bl_label = "MultiEdit Enter" 53 | bl_idname = "objects.multiedit_enter_operator" 54 | 55 | #The main function of the class: 56 | def execute(self, context): 57 | #Create a list with all the selected objects: 58 | selected_objects_list = bpy.context.selected_objects 59 | 60 | #Deselect all the non-mesh objects: 61 | for visible_object in selected_objects_list: 62 | if visible_object.type != "MESH": 63 | visible_object.select = False 64 | else: 65 | pass 66 | 67 | #Create a new list with all the selected objects: 68 | new_selected_objects_list = bpy.context.selected_objects 69 | 70 | #If there's only one selected object, enter Edit-mode, else create 71 | #a MultiEdit instance: 72 | if len(new_selected_objects_list) >= 2: 73 | 74 | #If the name_list contains elements, that means that there is 75 | #at least one more MultiEdit instance. 76 | if len(name_list) == 0: 77 | self.Create_MultiEdit(new_selected_objects_list) 78 | else: 79 | raise ValueError("A MultiEdit instance is already running!") 80 | else: 81 | bpy.ops.object.mode_set(mode = 'EDIT') 82 | return {'FINISHED'} 83 | 84 | #The function that actually initiated a new MultiEdit: 85 | def Create_MultiEdit(self, objects): 86 | 87 | #Create a variable to keep track of the number of objects: 88 | copied_index = 0 89 | 90 | #Iterate through the given objects list: 91 | for object in objects: 92 | 93 | #Append the object's name to the name_list: 94 | name_list.append(object.name) 95 | 96 | #Call the Duplicate_Object function: 97 | new_object_name = object.name + "_dupl" + str(copied_index) 98 | self.Duplicate_Object(bpy.context.scene, (new_object_name),object) 99 | duplicated_list.append(new_object_name) 100 | 101 | #Increase the copied_index by 1: 102 | copied_index += 1 103 | 104 | #Call the Create_Vertex_Groups function: 105 | self.Create_Vertex_Groups(object) 106 | 107 | #Move duplicated objects to diferent layer: 108 | bpy.ops.object.select_all(action = 'DESELECT') 109 | bpy.data.objects[new_object_name].select = True 110 | layers = [] 111 | for i in object.layers: 112 | layers.append(i) 113 | bpy.ops.object.move_to_layer(layers = layers) 114 | bpy.ops.object.hide_view_set() 115 | ##bpy.ops.object.move_to_layer(layers=((False,)*19 +(True,)))## 116 | 117 | #Call the Remove_Modifiers_and_Constraints function: 118 | self.Remove_Modifiers_and_Constraints(object) 119 | 120 | #Finally, join all the objects and enter Edit-mode: 121 | for object in objects: 122 | bpy.data.objects[object.name].select = True 123 | bpy.context.scene.objects.active = objects[0] 124 | bpy.ops.object.join() 125 | bpy.context.active_object.name = "MultiEdit" 126 | bpy.ops.object.mode_set(mode = 'EDIT') 127 | 128 | 129 | def Create_Vertex_Groups(self, object): 130 | 131 | ###Create the necessary vertex groups:### 132 | bpy.context.scene.objects.active = object 133 | for vertex_group in object.vertex_groups: 134 | special_vgroups_list.append(vertex_group.name) 135 | 136 | #Create vertex groups containting all the vertices: 137 | object.vertex_groups.new(object.name) 138 | vertex_group = object.vertex_groups[-1] 139 | verts = [vert.index for vert in object.data.vertices] 140 | vertex_group.add(verts, 1.0, 'ADD') 141 | 142 | #####for vert in object.data.vertices: 143 | ##### verts.append(vert.index) 144 | ##### vertex_group.add(verts, 1.0, 'ADD') 145 | 146 | 147 | def Remove_Modifiers_and_Constraints(self, object): 148 | 149 | ###Remove all the modifiers and constraints from the objects:### 150 | object.select = True 151 | for modifier in object.modifiers: 152 | object.modifiers.remove(modifier) 153 | for constraint in object.constraints: 154 | object.constraints.remove(constraint) 155 | 156 | def Duplicate_Object(self, scene, name, old_object): 157 | 158 | ###Duplicate the given object:### 159 | 160 | #Create new mesh: 161 | mesh = bpy.data.meshes.new(name) 162 | 163 | #Create new object associated with the mesh: 164 | ob_new = bpy.data.objects.new(name, mesh) 165 | 166 | #Copy data block from the old object into the new object: 167 | ob_new.data = old_object.data.copy() 168 | ob_new.scale = old_object.scale 169 | ob_new.rotation_euler = old_object.rotation_euler 170 | ob_new.location = old_object.location 171 | 172 | #Link new object to the given scene and select it 173 | scene.objects.link(ob_new) 174 | 175 | #Copy all the vertex groups: 176 | for vertex_g in old_object.vertex_groups: 177 | vert_g_new = ob_new.vertex_groups.new(vertex_g.name) 178 | properties = [p.identifier for p in vertex_g.bl_rna.properties 179 | if not p.is_readonly] 180 | for prop in properties: 181 | setattr(vert_g_new, prop, getattr(vertex_g, prop)) 182 | 183 | #Copy all the object groups: 184 | for group in old_object.users_group: 185 | bpy.context.scene.objects.active = ob_new 186 | bpy.ops.object.group_link(group=group.name) 187 | 188 | #Copy parent object value 189 | try: 190 | parents_list[old_object.name] = (old_object.parent).name 191 | except: 192 | pass 193 | 194 | #Copy all the modifiers: 195 | for mod in old_object.modifiers: 196 | mod_new = ob_new.modifiers.new(mod.name, mod.type) 197 | properties = [p.identifier for p in mod.bl_rna.properties 198 | if not p.is_readonly] 199 | for prop in properties: 200 | setattr(mod_new, prop, getattr(mod, prop)) 201 | 202 | #Copy all the constraints: 203 | for constr in old_object.constraints: 204 | constr_new = ob_new.constraints.new(constr.type) 205 | properties = [p.identifier for p in constr.bl_rna.properties 206 | if not p.is_readonly] 207 | for prop in properties: 208 | setattr(constr_new, prop, getattr(constr, prop)) 209 | 210 | #Finally, return the new object: 211 | return ob_new 212 | 213 | 214 | class MultiEdit_Exit(bpy.types.Operator): 215 | bl_label = "MultiEdit Exit" 216 | bl_idname = "objects.multiedit_exit_operator" 217 | bl_context = "editmode" 218 | 219 | #The main function of the class: 220 | def execute(self,context): 221 | 222 | #In case that any problems arise, just cancel the MultiEdit: 223 | try: 224 | bpy.context.scene.objects.active = bpy.data.objects["MultiEdit"] 225 | except: 226 | del name_list[:] 227 | del duplicated_list[:] 228 | del special_vgroups_list[:] 229 | parents_list.clear() 230 | 231 | #Create the necessary variables: 232 | active_object = bpy.context.active_object 233 | name = active_object.name 234 | vgroup_index = 0 235 | 236 | self.Separate_Objects(active_object, name, vgroup_index) 237 | self.Fix_Objects(active_object, name, vgroup_index) 238 | 239 | return {'FINISHED'} 240 | 241 | def Separate_Objects(self, active_object, name, vgroup_index): 242 | 243 | ###Separate given object according to the vertex_groups: 244 | for vertex_group in active_object.vertex_groups: 245 | bpy.ops.object.mode_set(mode = 'EDIT') 246 | bpy.ops.mesh.select_all(action = 'DESELECT') 247 | bpy.ops.mesh.select_mode(type="VERT") 248 | bpy.ops.object.mode_set(mode = 'OBJECT') 249 | 250 | for vert in active_object.data.vertices: 251 | for vertGroup in vert.groups: 252 | if vertGroup.group == vgroup_index: 253 | if vertex_group.name in special_vgroups_list: 254 | break 255 | else: 256 | vert.select = True 257 | else: 258 | pass 259 | 260 | 261 | 262 | bpy.ops.object.mode_set(mode = 'EDIT') 263 | try: 264 | bpy.ops.mesh.separate(type="SELECTED") 265 | except: 266 | pass 267 | vgroup_index += 1 268 | bpy.ops.object.mode_set(mode = 'OBJECT') 269 | 270 | 271 | def Fix_Objects(self, active_object, name, vgroup_index): 272 | 273 | existing_vg = [] 274 | object_layer = bpy.context.scene.active_layer 275 | for object in bpy.context.selected_objects: 276 | bpy.context.scene.objects.active = object 277 | del existing_vg[:] 278 | vgroup_index = 0 279 | for vg in object.vertex_groups: 280 | bpy.ops.object.mode_set(mode = 'EDIT') 281 | bpy.ops.mesh.select_all(action = 'DESELECT') 282 | bpy.ops.object.mode_set(mode = 'OBJECT') 283 | #Finding which vertex group has verts 284 | for vert in object.data.vertices: 285 | for vertGroup in vert.groups: 286 | if vertGroup.group == vgroup_index: 287 | vert.select = True 288 | if object.vertex_groups[vgroup_index].name in special_vgroups_list: 289 | pass 290 | elif object.vertex_groups[vgroup_index].name in existing_vg: 291 | pass 292 | else: 293 | existing_vg.append(object.vertex_groups[vgroup_index].name) 294 | 295 | vgroup_index += 1 296 | 297 | #RENAME OBJECTS, ORGANIZES MATERIALS 298 | if len(existing_vg) < 2 and len(existing_vg) > 0: 299 | # try: 300 | object.name = existing_vg[0] 301 | wanted_object_name = duplicated_list[(name_list.index(object.name))] 302 | 303 | mats = [] 304 | 305 | for slot in object.material_slots: 306 | for slot_dupl in bpy.data.objects[wanted_object_name].material_slots: 307 | if slot_dupl.name == slot.name: 308 | mats.append(slot_dupl.name) 309 | else: 310 | pass 311 | 312 | var = 0 313 | for mat_index in range(len(object.material_slots)): 314 | try: 315 | if object.material_slots[mat_index - var].name in mats: 316 | pass 317 | else: 318 | bpy.context.scene.objects.active = object 319 | #mat_index = object.material_slots[slot_dupl].index 320 | object.active_material_index = mat_index - var 321 | bpy.ops.object.material_slot_remove() 322 | var +=1 323 | except: 324 | pass 325 | 326 | #Call the Copy_Data function: 327 | self.Copy_Data(wanted_object_name, object) 328 | 329 | else: 330 | object.name = "New Geometry" 331 | 332 | #Call the Delete_Objects function: 333 | self.Delete_Objects() 334 | 335 | #Call the Preserve_Data function: 336 | self.Preserve_Data() 337 | 338 | #Unhide all objects: 339 | bpy.ops.object.hide_view_clear() 340 | 341 | #Delete duplicated objects 342 | for obj in bpy.context.selected_objects: 343 | if "dupl" not in obj.name: 344 | obj.hide = True 345 | bpy.ops.object.delete() 346 | 347 | #Empty lists for future use 348 | del name_list[:] 349 | del duplicated_list[:] 350 | del special_vgroups_list[:] 351 | parents_list.clear() 352 | 353 | def Copy_Data(self, wanted_object_name, object): 354 | 355 | ###All those things copy modifiers, constraints etc. with all properties. Cool...### 356 | bpy.ops.object.select_all(action = 'DESELECT') 357 | object.select = True 358 | layers = [] 359 | for i in bpy.data.objects[wanted_object_name].layers: 360 | layers.append(i) 361 | bpy.ops.object.move_to_layer(layers = layers) 362 | 363 | #Copy modifiers: 364 | properties = [] 365 | for mod in bpy.data.objects[wanted_object_name].modifiers: 366 | mod_new = object.modifiers.new(mod.name, mod.type) 367 | properties = [p.identifier for p in mod.bl_rna.properties 368 | if not p.is_readonly] 369 | for prop in properties: 370 | setattr(mod_new, prop, getattr(mod, prop)) 371 | 372 | #Remove all existing groups: 373 | bpy.context.scene.objects.active = object 374 | bpy.ops.group.objects_remove_all() 375 | 376 | #Copy object groups: 377 | for group in bpy.data.objects[wanted_object_name].users_group: 378 | bpy.context.scene.objects.active = object 379 | bpy.ops.object.group_link(group=group.name) 380 | 381 | #Delete unnecessary vertex groups: 382 | for vg in object.vertex_groups: 383 | if vg.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].vertex_groups: 384 | pass 385 | else: 386 | object.vertex_groups.remove(vg) 387 | 388 | #Copy constraints: 389 | for constr in bpy.data.objects[wanted_object_name].constraints: 390 | constr_new = object.constraints.new(constr.type) 391 | properties = [p.identifier for p in constr.bl_rna.properties 392 | if not p.is_readonly] 393 | for prop in properties: 394 | setattr(constr_new, prop, getattr(constr, prop)) 395 | 396 | #Copy shape keys: 397 | try: 398 | for shape_key in object.data.shape_keys.key_blocks: 399 | try: 400 | bpy.context.scene.objects.active = object 401 | if shape_key.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].data.shape_keys.key_blocks: 402 | pass 403 | else: 404 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 405 | object.active_shape_key_index = idx 406 | bpy.ops.object.shape_key_remove() 407 | except: 408 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 409 | object.active_shape_key_index = idx 410 | bpy.ops.object.shape_key_remove() 411 | except: 412 | pass 413 | 414 | def Preserve_Data(self): 415 | #Check if checkbox is true and preserve or not the rotation/scale values of the objects: 416 | if bpy.context.scene.Preserve_Location_Rotation_Scale: 417 | for obj in bpy.data.objects: 418 | self.Preserve_Parents(obj) 419 | for nam in name_list: 420 | if nam == obj.name: 421 | obj.select = True 422 | bpy.context.scene.objects.active = obj 423 | 424 | #Location: 425 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 426 | #Rotation: 427 | rotation_values = bpy.data.objects[duplicated_list[name_list.index(nam)]].rotation_euler 428 | 429 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 430 | 431 | rot = (rotation_values[0]*(-1), 432 | rotation_values[1]*(-1), 433 | rotation_values[2]*(-1)) 434 | 435 | obj.rotation_mode = 'ZYX' 436 | obj.rotation_euler = (rot) 437 | 438 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 439 | 440 | rot = (rotation_values[0], 441 | rotation_values[1], 442 | rotation_values[2]) 443 | 444 | obj.rotation_mode = 'XYZ' 445 | obj.rotation_euler = (rot) 446 | 447 | #Scale/Dimensions: 448 | scales = bpy.data.objects[duplicated_list[name_list.index(nam)]].scale 449 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 450 | bpy.ops.object.mode_set(mode = 'EDIT') 451 | bpy.ops.mesh.select_all(action = 'SELECT') 452 | bpy.ops.transform.resize(value=(1.0/scales[0], 1.0/scales[1], 1.0/scales[2])) 453 | bpy.ops.object.mode_set(mode = 'OBJECT') 454 | obj.scale = scales 455 | bpy.ops.object.mode_set(mode = 'EDIT') 456 | bpy.ops.object.mode_set(mode = 'OBJECT') 457 | 458 | bpy.context.scene.cursor_location = bpy.data.objects[duplicated_list[name_list.index(nam)]].location 459 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR") 460 | 461 | #Deselect object: 462 | obj.select = False 463 | 464 | else: 465 | pass 466 | 467 | else: 468 | for obj in bpy.data.objects: 469 | for nam in name_list: 470 | self.Preserve_Parents(obj) 471 | if nam in obj.name: 472 | obj.select = True 473 | 474 | #Location: 475 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 476 | 477 | #Rotation/Scale: 478 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 479 | 480 | else: 481 | pass 482 | 483 | #A function to preserve an object's parent: 484 | def Preserve_Parents(self, obj): 485 | try: 486 | obj.parent = bpy.data.objects[parents_list[obj.name]] 487 | except: 488 | pass 489 | 490 | #Quite simple. Deletes the objects that don't have any geometry 491 | def Delete_Objects(self): 492 | 493 | bpy.ops.object.select_all(action="DESELECT") 494 | try: 495 | bpy.data.objects["New Geometry"].select = True 496 | vert_check = bpy.data.objects["New Geometry"] 497 | if len(vert_check.data.vertices) > 0: 498 | self.Clear_New_Geometry_Data("New Geometry") 499 | else: 500 | bpy.ops.object.delete() 501 | except: 502 | pass 503 | 504 | bpy.ops.object.select_all(action="DESELECT") 505 | try: 506 | bpy.data.objects["MultiEdit"].select = True 507 | vert_check = bpy.data.objects["MultiEdit"] 508 | if len(vert_check.data.vertices) > 0: 509 | bpy.data.objects["MultiEdit"].name = "New Geometry" 510 | self.Clear_New_Geometry_Data("New Geometry") 511 | else: 512 | bpy.ops.object.delete() 513 | except: 514 | pass 515 | 516 | #Clears all the new geometry's data. 517 | def Clear_New_Geometry_Data(self, obj_name): 518 | main_object = bpy.data.objects[obj_name] 519 | 520 | #Remove object groups: 521 | bpy.context.scene.objects.active = main_object 522 | bpy.ops.group.objects_remove_all() 523 | 524 | #Remove vertex groups: 525 | for vgroup in main_object.vertex_groups: 526 | main_object.vertex_groups.remove(vgroup) 527 | 528 | #Remove UV layers: 529 | for uv in main_object.data.uv_textures: 530 | main_object.data.uv_textures.remove(uv) 531 | 532 | #Set origin point to center of geometry: 533 | main_object.select = True 534 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 535 | 536 | #Remove materials: 537 | while main_object.data.materials: 538 | main_object.data.materials.pop() 539 | 540 | 541 | """ 542 | 543 | This Operator is modal and checks automatically if it should enter or exit 544 | MultiEdit mode 545 | 546 | """ 547 | 548 | 549 | class MultiEditModal(bpy.types.Operator): 550 | """Translate the view using mouse events""" 551 | 552 | bl_label = "MultiEdit mode start" 553 | bl_idname = "objects.multieditmodal" 554 | bl_context = "editmode" 555 | multiEditMode= False 556 | editingObjects =[] 557 | timeLapse = 0 558 | 559 | def execute(self, context): 560 | pass 561 | return 562 | 563 | def modal(self, context, event): 564 | 565 | 566 | 567 | # do not check anything if the mouse cursor is not in Viewport 568 | if context.space_data.type == 'VIEW_3D': 569 | 570 | # check every one second 571 | if time.time() > self.timeLapse + 0.3: 572 | self.timeLapse = round(time.time()) 573 | self.editingObjects = [] 574 | 575 | for obj in bpy.data.objects: 576 | 577 | if obj.mode == 'EDIT' and bpy.context.scene.multi_edit_enable: 578 | self.editingObjects.append(obj) 579 | 580 | # if in edit mode and multiple selected enter MultiEdit mode 581 | if obj.select and len( 582 | self.editingObjects) > 0 and self.multiEditMode == False and bpy.context.scene.multi_edit_enable: 583 | self.editingObjects.append(obj) 584 | self.multiEditMode = True 585 | bpy.ops.object.mode_set(mode='OBJECT') 586 | bpy.ops.objects.multiedit_enter_operator() 587 | 588 | # if in object mode , no objects selected and MulitEditMode previously 589 | # enabled , then exit MultiEdit mode 590 | if len(self.editingObjects) == 0 and self.multiEditMode: 591 | self.multiEditMode = False 592 | bpy.ops.objects.multiedit_exit_operator() 593 | 594 | 595 | 596 | 597 | # do not capture any key or mouse event 598 | return {'PASS_THROUGH'} 599 | 600 | def invoke(self, context, event): 601 | 602 | if context.space_data.type == 'VIEW_3D': 603 | v3d = context.space_data 604 | rv3d = v3d.region_3d 605 | 606 | 607 | 608 | context.window_manager.modal_handler_add(self) 609 | return {'RUNNING_MODAL'} 610 | else: 611 | self.report({'WARNING'}, "Active space must be a View3d") 612 | return {'CANCELLED'} 613 | 614 | 615 | 616 | # Main Panel for MultiEdit mode 617 | class MultiEdit_Panel(bpy.types.Panel): 618 | bl_label = "Multiple Objects Editing" 619 | bl_idname = "MultiEdit" 620 | bl_space_type = "VIEW_3D" 621 | bl_region_type = "TOOLS" 622 | bl_category = "Tools" 623 | first_draw=True 624 | 625 | def draw(self, context): 626 | layout = self.layout 627 | sce = bpy.context.scene 628 | 629 | layout.operator(MultiEdit_Enter.bl_idname) 630 | layout.operator(MultiEdit_Exit.bl_idname) 631 | layout.operator(MultiEditModal.bl_idname) 632 | layout.prop(sce, "Preserve_Location_Rotation_Scale", toggle=True, icon='ROTATE') 633 | layout.prop(sce, "multi_edit_enable",toggle=True,icon='EDIT') 634 | 635 | 636 | def register(): 637 | bpy.utils.register_class(MultiEdit_Enter) 638 | bpy.utils.register_class(MultiEdit_Exit) 639 | bpy.utils.register_class(MultiEdit_Panel) 640 | bpy.types.Scene.Preserve_Location_Rotation_Scale = bpy.props.BoolProperty \ 641 | ( 642 | name = "Preserve Location/Rotation/Scale", 643 | description = "Preserve the Location/Rotation/Scale values of the objects.", 644 | default = True 645 | ) 646 | # Is MultiEdit enabled 647 | bpy.types.Scene.multi_edit_enable = bpy.props.BoolProperty \ 648 | ( 649 | name = "Enable Multi Edit mode", 650 | description = "Can automatically detect if multiple objects are selected and edit them together.", 651 | default = True 652 | ) 653 | bpy.utils.register_class(MultiEditModal) 654 | 655 | def unregister(): 656 | bpy.utils.unregister_class(MultiEdit_Enter); 657 | bpy.utils.unregister_class(MultiEdit_Exit); 658 | bpy.utils.unregister_class(MultiEdit_Panel); 659 | bpy.utils.unregister_class(MultiEditModal) 660 | 661 | if __name__ == "__main__": 662 | register() 663 | -------------------------------------------------------------------------------- /MultiEdit_1_1.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": "MultiEdit", 21 | "author": "Antonis Karvelas, Dimitris Chloupis", 22 | "version": (1, 0), 23 | "blender": (2, 7, 8), 24 | "location": "VIEW 3D > Tools > Multiple Objects Editing ", 25 | "description": "Allows you to edit multiple objects together in edit mode without destroying data.", 26 | "warning": "Could still have a few issues, be careful.", 27 | "wiki_url": "", 28 | "tracker_url": "", 29 | "category": "Mesh"} 30 | 31 | #Imports: 32 | import bpy 33 | import math 34 | from mathutils import Vector 35 | from bpy.props import FloatVectorProperty 36 | import time 37 | 38 | #Create a list to put the names of the selected objects: 39 | name_list = [] 40 | 41 | #Create a list to put the names of the duplicated objects: 42 | duplicated_list = [] 43 | 44 | #Create a list to put the vertex_groups that need to be maintained: 45 | special_vgroups_list = [] 46 | 47 | #Create a dictionary to put the parents 48 | parents_list = {} 49 | 50 | #Create a dictionary to put the mesh name, connected with the object name: 51 | mesh_name = {} 52 | 53 | 54 | class MultiEdit_Enter(bpy.types.Operator): 55 | bl_label = "MultiEdit Enter" 56 | bl_idname = "objects.multiedit_enter_operator" 57 | 58 | #The main function of the class: 59 | def execute(self, context): 60 | #Create a list with all the selected objects: 61 | selected_objects_list = bpy.context.selected_objects 62 | 63 | #Deselect all the non-mesh objects: 64 | for visible_object in selected_objects_list: 65 | if visible_object.type != "MESH": 66 | visible_object.select = False 67 | else: 68 | pass 69 | 70 | #Create a new list with all the selected objects: 71 | new_selected_objects_list = bpy.context.selected_objects 72 | 73 | #If there's only one selected object, enter Edit-mode, else create 74 | #a MultiEdit instance: 75 | if len(new_selected_objects_list) >= 2: 76 | 77 | #If the name_list contains elements, that means that there is 78 | #at least one more MultiEdit instance. 79 | if len(name_list) == 0: 80 | self.Create_MultiEdit(new_selected_objects_list) 81 | else: 82 | raise ValueError("A MultiEdit instance is already running!") 83 | else: 84 | bpy.ops.object.mode_set(mode = 'EDIT') 85 | return {'FINISHED'} 86 | 87 | #The function that actually initiated a new MultiEdit: 88 | def Create_MultiEdit(self, objects): 89 | 90 | #Create a variable to keep track of the number of objects: 91 | copied_index = 0 92 | 93 | #Iterate through the given objects list: 94 | for object in objects: 95 | 96 | #Append the object's name to the name_list: 97 | name_list.append(object.name) 98 | 99 | #Call the Duplicate_Object function: 100 | new_object_name = object.name + "_dupl" + str(copied_index) 101 | self.Duplicate_Object(bpy.context.scene, (new_object_name),object) 102 | duplicated_list.append(new_object_name) 103 | 104 | #Increase the copied_index by 1: 105 | copied_index += 1 106 | 107 | #Call the Create_Vertex_Groups function: 108 | self.Create_Vertex_Groups(object) 109 | 110 | #Move duplicated objects to diferent layer: 111 | bpy.ops.object.select_all(action = 'DESELECT') 112 | bpy.data.objects[new_object_name].select = True 113 | layers = [] 114 | for i in object.layers: 115 | layers.append(i) 116 | bpy.ops.object.move_to_layer(layers = layers) 117 | bpy.ops.object.hide_view_set() 118 | ##bpy.ops.object.move_to_layer(layers=((False,)*19 +(True,)))## 119 | 120 | #Call the Remove_Modifiers_and_Constraints function: 121 | self.Remove_Modifiers_and_Constraints(object) 122 | 123 | #Finally, join all the objects and enter Edit-mode: 124 | for object in objects: 125 | bpy.data.objects[object.name].select = True 126 | bpy.context.scene.objects.active = objects[0] 127 | bpy.ops.object.join() 128 | bpy.context.active_object.name = "MultiEdit" 129 | bpy.ops.object.mode_set(mode = 'EDIT') 130 | 131 | 132 | def Create_Vertex_Groups(self, object): 133 | 134 | ###Create the necessary vertex groups:### 135 | bpy.context.scene.objects.active = object 136 | for vertex_group in object.vertex_groups: 137 | special_vgroups_list.append(vertex_group.name) 138 | 139 | #Create vertex groups containting all the vertices: 140 | object.vertex_groups.new(object.name) 141 | vertex_group = object.vertex_groups[-1] 142 | verts = [vert.index for vert in object.data.vertices] 143 | vertex_group.add(verts, 1.0, 'ADD') 144 | 145 | #####for vert in object.data.vertices: 146 | ##### verts.append(vert.index) 147 | ##### vertex_group.add(verts, 1.0, 'ADD') 148 | 149 | 150 | def Remove_Modifiers_and_Constraints(self, object): 151 | 152 | ###Remove all the modifiers and constraints from the objects:### 153 | object.select = True 154 | for modifier in object.modifiers: 155 | object.modifiers.remove(modifier) 156 | for constraint in object.constraints: 157 | object.constraints.remove(constraint) 158 | 159 | def Duplicate_Object(self, scene, name, old_object): 160 | 161 | ###Duplicate the given object:### 162 | 163 | #Create new mesh: 164 | mesh = bpy.data.meshes.new(name) 165 | 166 | #Create new object associated with the mesh: 167 | ob_new = bpy.data.objects.new(name, mesh) 168 | 169 | #Copy data block from the old object into the new object: 170 | ob_new.data = old_object.data.copy() 171 | ob_new.scale = old_object.scale 172 | ob_new.rotation_euler = old_object.rotation_euler 173 | ob_new.location = old_object.location 174 | 175 | #Link new object to the given scene and select it 176 | scene.objects.link(ob_new) 177 | 178 | #Copy all the vertex groups: 179 | for vertex_g in old_object.vertex_groups: 180 | vert_g_new = ob_new.vertex_groups.new(vertex_g.name) 181 | properties = [p.identifier for p in vertex_g.bl_rna.properties 182 | if not p.is_readonly] 183 | for prop in properties: 184 | setattr(vert_g_new, prop, getattr(vertex_g, prop)) 185 | 186 | #Copy all the object groups: 187 | for group in old_object.users_group: 188 | bpy.context.scene.objects.active = ob_new 189 | bpy.ops.object.group_link(group=group.name) 190 | 191 | #Copy parent object value 192 | try: 193 | parents_list[old_object.name] = (old_object.parent).name 194 | except: 195 | pass 196 | 197 | #Copy mesh name 198 | mesh_name[old_object.name] = old_object.data.name 199 | 200 | #Copy all the modifiers: 201 | for mod in old_object.modifiers: 202 | mod_new = ob_new.modifiers.new(mod.name, mod.type) 203 | properties = [p.identifier for p in mod.bl_rna.properties 204 | if not p.is_readonly] 205 | for prop in properties: 206 | setattr(mod_new, prop, getattr(mod, prop)) 207 | 208 | #Copy all the constraints: 209 | for constr in old_object.constraints: 210 | constr_new = ob_new.constraints.new(constr.type) 211 | properties = [p.identifier for p in constr.bl_rna.properties 212 | if not p.is_readonly] 213 | for prop in properties: 214 | setattr(constr_new, prop, getattr(constr, prop)) 215 | 216 | #Finally, return the new object: 217 | return ob_new 218 | 219 | 220 | class MultiEdit_Exit(bpy.types.Operator): 221 | bl_label = "MultiEdit Exit" 222 | bl_idname = "objects.multiedit_exit_operator" 223 | bl_context = "editmode" 224 | 225 | #The main function of the class: 226 | def execute(self,context): 227 | 228 | #In case that any problems arise, just cancel the MultiEdit: 229 | try: 230 | bpy.context.scene.objects.active = bpy.data.objects["MultiEdit"] 231 | except: 232 | del name_list[:] 233 | del duplicated_list[:] 234 | del special_vgroups_list[:] 235 | parents_list.clear() 236 | mesh_name.clear() 237 | 238 | #Create the necessary variables: 239 | active_object = bpy.context.active_object 240 | name = active_object.name 241 | vgroup_index = 0 242 | 243 | self.Separate_Objects(active_object, name, vgroup_index) 244 | self.Fix_Objects(active_object, name, vgroup_index) 245 | 246 | return {'FINISHED'} 247 | 248 | def Separate_Objects(self, active_object, name, vgroup_index): 249 | 250 | ###Separate given object according to the vertex_groups: 251 | for vertex_group in active_object.vertex_groups: 252 | bpy.ops.object.mode_set(mode = 'EDIT') 253 | bpy.ops.mesh.select_all(action = 'DESELECT') 254 | bpy.ops.mesh.select_mode(type="VERT") 255 | bpy.ops.object.mode_set(mode = 'OBJECT') 256 | 257 | for vert in active_object.data.vertices: 258 | for vertGroup in vert.groups: 259 | if vertGroup.group == vgroup_index: 260 | if vertex_group.name in special_vgroups_list: 261 | break 262 | else: 263 | vert.select = True 264 | else: 265 | pass 266 | 267 | 268 | 269 | bpy.ops.object.mode_set(mode = 'EDIT') 270 | try: 271 | bpy.ops.mesh.separate(type="SELECTED") 272 | except: 273 | pass 274 | vgroup_index += 1 275 | bpy.ops.object.mode_set(mode = 'OBJECT') 276 | 277 | 278 | def Fix_Objects(self, active_object, name, vgroup_index): 279 | 280 | existing_vg = [] 281 | object_layer = bpy.context.scene.active_layer 282 | for object in bpy.context.selected_objects: 283 | bpy.context.scene.objects.active = object 284 | del existing_vg[:] 285 | vgroup_index = 0 286 | for vg in object.vertex_groups: 287 | bpy.ops.object.mode_set(mode = 'EDIT') 288 | bpy.ops.mesh.select_all(action = 'DESELECT') 289 | bpy.ops.object.mode_set(mode = 'OBJECT') 290 | #Finding which vertex group has verts 291 | for vert in object.data.vertices: 292 | for vertGroup in vert.groups: 293 | if vertGroup.group == vgroup_index: 294 | vert.select = True 295 | if object.vertex_groups[vgroup_index].name in special_vgroups_list: 296 | pass 297 | elif object.vertex_groups[vgroup_index].name in existing_vg: 298 | pass 299 | else: 300 | existing_vg.append(object.vertex_groups[vgroup_index].name) 301 | 302 | vgroup_index += 1 303 | 304 | #RENAME OBJECTS, ORGANIZES MATERIALS 305 | if len(existing_vg) < 2 and len(existing_vg) > 0: 306 | # try: 307 | object.name = existing_vg[0] 308 | wanted_object_name = duplicated_list[(name_list.index(object.name))] 309 | 310 | mats = [] 311 | 312 | for slot in object.material_slots: 313 | for slot_dupl in bpy.data.objects[wanted_object_name].material_slots: 314 | if slot_dupl.name == slot.name: 315 | mats.append(slot_dupl.name) 316 | else: 317 | pass 318 | 319 | var = 0 320 | for mat_index in range(len(object.material_slots)): 321 | try: 322 | if object.material_slots[mat_index - var].name in mats: 323 | pass 324 | else: 325 | bpy.context.scene.objects.active = object 326 | #mat_index = object.material_slots[slot_dupl].index 327 | object.active_material_index = mat_index - var 328 | bpy.ops.object.material_slot_remove() 329 | var +=1 330 | except: 331 | pass 332 | 333 | #Call the Copy_Data function: 334 | self.Copy_Data(wanted_object_name, object) 335 | 336 | else: 337 | object.name = "New Geometry" 338 | 339 | #Fix mesh name 340 | if(object.name in mesh_name): 341 | object.data.name = mesh_name[object.name] 342 | 343 | #Call the Delete_Objects function: 344 | self.Delete_Objects() 345 | 346 | #Call the Preserve_Data function: 347 | self.Preserve_Data() 348 | 349 | #Unhide all objects: 350 | bpy.ops.object.hide_view_clear() 351 | 352 | #Delete duplicated objects 353 | for obj in bpy.context.selected_objects: 354 | if "dupl" not in obj.name: 355 | obj.hide = True 356 | bpy.ops.object.delete() 357 | 358 | #Empty lists for future use 359 | del name_list[:] 360 | del duplicated_list[:] 361 | del special_vgroups_list[:] 362 | parents_list.clear() 363 | mesh_name.clear() 364 | 365 | def Copy_Data(self, wanted_object_name, object): 366 | 367 | ###All those things copy modifiers, constraints etc. with all properties. Cool...### 368 | bpy.ops.object.select_all(action = 'DESELECT') 369 | object.select = True 370 | layers = [] 371 | for i in bpy.data.objects[wanted_object_name].layers: 372 | layers.append(i) 373 | bpy.ops.object.move_to_layer(layers = layers) 374 | 375 | #Copy modifiers: 376 | properties = [] 377 | for mod in bpy.data.objects[wanted_object_name].modifiers: 378 | mod_new = object.modifiers.new(mod.name, mod.type) 379 | properties = [p.identifier for p in mod.bl_rna.properties 380 | if not p.is_readonly] 381 | for prop in properties: 382 | setattr(mod_new, prop, getattr(mod, prop)) 383 | 384 | #Remove all existing groups: 385 | bpy.context.scene.objects.active = object 386 | bpy.ops.group.objects_remove_all() 387 | 388 | #Copy object groups: 389 | for group in bpy.data.objects[wanted_object_name].users_group: 390 | bpy.context.scene.objects.active = object 391 | bpy.ops.object.group_link(group=group.name) 392 | 393 | #Delete unnecessary vertex groups: 394 | for vg in object.vertex_groups: 395 | if vg.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].vertex_groups: 396 | pass 397 | else: 398 | object.vertex_groups.remove(vg) 399 | 400 | #Copy constraints: 401 | for constr in bpy.data.objects[wanted_object_name].constraints: 402 | constr_new = object.constraints.new(constr.type) 403 | properties = [p.identifier for p in constr.bl_rna.properties 404 | if not p.is_readonly] 405 | for prop in properties: 406 | setattr(constr_new, prop, getattr(constr, prop)) 407 | 408 | #Copy shape keys: 409 | try: 410 | for shape_key in object.data.shape_keys.key_blocks: 411 | try: 412 | bpy.context.scene.objects.active = object 413 | if shape_key.name in bpy.data.objects[(duplicated_list[(name_list.index(object.name))])].data.shape_keys.key_blocks: 414 | pass 415 | else: 416 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 417 | object.active_shape_key_index = idx 418 | bpy.ops.object.shape_key_remove() 419 | except: 420 | idx = object.data.shape_keys.key_blocks.keys().index(shape_key.name) 421 | object.active_shape_key_index = idx 422 | bpy.ops.object.shape_key_remove() 423 | except: 424 | pass 425 | 426 | def Preserve_Data(self): 427 | #Check if checkbox is true and preserve or not the rotation/scale values of the objects: 428 | if bpy.context.scene.Preserve_Location_Rotation_Scale: 429 | for obj in bpy.data.objects: 430 | self.Preserve_Parents(obj) 431 | for nam in name_list: 432 | if nam == obj.name: 433 | obj.select = True 434 | bpy.context.scene.objects.active = obj 435 | 436 | #Location: 437 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 438 | #Rotation: 439 | rotation_values = bpy.data.objects[duplicated_list[name_list.index(nam)]].rotation_euler 440 | 441 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 442 | 443 | rot = (rotation_values[0]*(-1), 444 | rotation_values[1]*(-1), 445 | rotation_values[2]*(-1)) 446 | 447 | obj.rotation_mode = 'ZYX' 448 | obj.rotation_euler = (rot) 449 | 450 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) 451 | 452 | rot = (rotation_values[0], 453 | rotation_values[1], 454 | rotation_values[2]) 455 | 456 | obj.rotation_mode = 'XYZ' 457 | obj.rotation_euler = (rot) 458 | 459 | #Scale/Dimensions: 460 | scales = bpy.data.objects[duplicated_list[name_list.index(nam)]].scale 461 | bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) 462 | bpy.ops.object.mode_set(mode = 'EDIT') 463 | bpy.ops.mesh.select_all(action = 'SELECT') 464 | bpy.ops.transform.resize(value=(1.0/scales[0], 1.0/scales[1], 1.0/scales[2])) 465 | bpy.ops.object.mode_set(mode = 'OBJECT') 466 | obj.scale = scales 467 | bpy.ops.object.mode_set(mode = 'EDIT') 468 | bpy.ops.object.mode_set(mode = 'OBJECT') 469 | 470 | bpy.context.scene.cursor_location = bpy.data.objects[duplicated_list[name_list.index(nam)]].location 471 | bpy.ops.object.origin_set(type="ORIGIN_CURSOR") 472 | 473 | #Deselect object: 474 | obj.select = False 475 | 476 | else: 477 | pass 478 | 479 | else: 480 | for obj in bpy.data.objects: 481 | for nam in name_list: 482 | self.Preserve_Parents(obj) 483 | if nam in obj.name: 484 | obj.select = True 485 | 486 | #Location: 487 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 488 | 489 | #Rotation/Scale: 490 | bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) 491 | 492 | else: 493 | pass 494 | 495 | #A function to preserve an object's parent: 496 | def Preserve_Parents(self, obj): 497 | try: 498 | obj.parent = bpy.data.objects[parents_list[obj.name]] 499 | except: 500 | pass 501 | 502 | #Quite simple. Deletes the objects that don't have any geometry 503 | def Delete_Objects(self): 504 | 505 | bpy.ops.object.select_all(action="DESELECT") 506 | try: 507 | bpy.data.objects["New Geometry"].select = True 508 | vert_check = bpy.data.objects["New Geometry"] 509 | if len(vert_check.data.vertices) > 0: 510 | self.Clear_New_Geometry_Data("New Geometry") 511 | else: 512 | bpy.ops.object.delete() 513 | except: 514 | pass 515 | 516 | bpy.ops.object.select_all(action="DESELECT") 517 | try: 518 | bpy.data.objects["MultiEdit"].select = True 519 | vert_check = bpy.data.objects["MultiEdit"] 520 | if len(vert_check.data.vertices) > 0: 521 | bpy.data.objects["MultiEdit"].name = "New Geometry" 522 | self.Clear_New_Geometry_Data("New Geometry") 523 | else: 524 | bpy.ops.object.delete() 525 | except: 526 | pass 527 | 528 | #Clears all the new geometry's data. 529 | def Clear_New_Geometry_Data(self, obj_name): 530 | main_object = bpy.data.objects[obj_name] 531 | 532 | #Remove object groups: 533 | bpy.context.scene.objects.active = main_object 534 | bpy.ops.group.objects_remove_all() 535 | 536 | #Remove vertex groups: 537 | for vgroup in main_object.vertex_groups: 538 | main_object.vertex_groups.remove(vgroup) 539 | 540 | #Remove UV layers: 541 | for uv in main_object.data.uv_textures: 542 | main_object.data.uv_textures.remove(uv) 543 | 544 | #Set origin point to center of geometry: 545 | main_object.select = True 546 | bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY") 547 | 548 | #Remove materials: 549 | while main_object.data.materials: 550 | main_object.data.materials.pop() 551 | 552 | 553 | """ 554 | 555 | This Operator is modal and checks automatically if it should enter or exit 556 | MultiEdit mode 557 | 558 | """ 559 | 560 | 561 | class MultiEditModal(bpy.types.Operator): 562 | """Translate the view using mouse events""" 563 | 564 | bl_label = "MultiEdit mode start" 565 | bl_idname = "objects.multieditmodal" 566 | bl_context = "editmode" 567 | multiEditMode= False 568 | editingObjects =[] 569 | timeLapse = 0 570 | 571 | def execute(self, context): 572 | pass 573 | return 574 | 575 | def modal(self, context, event): 576 | 577 | 578 | 579 | # do not check anything if the mouse cursor is not in Viewport 580 | if context.space_data.type == 'VIEW_3D': 581 | 582 | # check every one second 583 | if time.time() > self.timeLapse + 0.3: 584 | self.timeLapse = round(time.time()) 585 | self.editingObjects = [] 586 | 587 | for obj in bpy.data.objects: 588 | 589 | if obj.mode == 'EDIT' and bpy.context.scene.multi_edit_enable: 590 | self.editingObjects.append(obj) 591 | 592 | # if in edit mode and multiple selected enter MultiEdit mode 593 | if obj.select and len( 594 | self.editingObjects) > 0 and self.multiEditMode == False and bpy.context.scene.multi_edit_enable: 595 | self.editingObjects.append(obj) 596 | self.multiEditMode = True 597 | bpy.ops.object.mode_set(mode='OBJECT') 598 | bpy.ops.objects.multiedit_enter_operator() 599 | 600 | # if in object mode , no objects selected and MulitEditMode previously 601 | # enabled , then exit MultiEdit mode 602 | if len(self.editingObjects) == 0 and self.multiEditMode: 603 | self.multiEditMode = False 604 | bpy.ops.objects.multiedit_exit_operator() 605 | 606 | 607 | 608 | 609 | # do not capture any key or mouse event 610 | return {'PASS_THROUGH'} 611 | 612 | def invoke(self, context, event): 613 | 614 | if context.space_data.type == 'VIEW_3D': 615 | v3d = context.space_data 616 | rv3d = v3d.region_3d 617 | 618 | 619 | 620 | context.window_manager.modal_handler_add(self) 621 | return {'RUNNING_MODAL'} 622 | else: 623 | self.report({'WARNING'}, "Active space must be a View3d") 624 | return {'CANCELLED'} 625 | 626 | 627 | 628 | # Main Panel for MultiEdit mode 629 | class MultiEdit_Panel(bpy.types.Panel): 630 | bl_label = "Multiple Objects Editing" 631 | bl_idname = "MultiEdit" 632 | bl_space_type = "VIEW_3D" 633 | bl_region_type = "TOOLS" 634 | bl_category = "Tools" 635 | first_draw=True 636 | 637 | def draw(self, context): 638 | layout = self.layout 639 | sce = bpy.context.scene 640 | 641 | layout.operator(MultiEdit_Enter.bl_idname) 642 | layout.operator(MultiEdit_Exit.bl_idname) 643 | layout.operator(MultiEditModal.bl_idname) 644 | layout.prop(sce, "Preserve_Location_Rotation_Scale", toggle=True, icon='ROTATE') 645 | layout.prop(sce, "multi_edit_enable",toggle=True,icon='EDIT') 646 | 647 | 648 | def register(): 649 | bpy.utils.register_class(MultiEdit_Enter) 650 | bpy.utils.register_class(MultiEdit_Exit) 651 | bpy.utils.register_class(MultiEdit_Panel) 652 | bpy.types.Scene.Preserve_Location_Rotation_Scale = bpy.props.BoolProperty \ 653 | ( 654 | name = "Preserve Location/Rotation/Scale", 655 | description = "Preserve the Location/Rotation/Scale values of the objects.", 656 | default = True 657 | ) 658 | # Is MultiEdit enabled 659 | bpy.types.Scene.multi_edit_enable = bpy.props.BoolProperty \ 660 | ( 661 | name = "Enable Multi Edit mode", 662 | description = "Can automatically detect if multiple objects are selected and edit them together.", 663 | default = True 664 | ) 665 | bpy.utils.register_class(MultiEditModal) 666 | 667 | def unregister(): 668 | bpy.utils.unregister_class(MultiEdit_Enter); 669 | bpy.utils.unregister_class(MultiEdit_Exit); 670 | bpy.utils.unregister_class(MultiEdit_Panel); 671 | bpy.utils.unregister_class(MultiEditModal) 672 | 673 | if __name__ == "__main__": 674 | register() 675 | --------------------------------------------------------------------------------