1import bpy 2 3from bpy.types import Operator 4from bpy.props import ( 5 StringProperty, 6 BoolProperty, 7 EnumProperty, 8 IntProperty, 9 FloatProperty 10 ) 11 12 13from .enum_values import * 14from .functions import * 15 16from math import radians 17 18# ----------------------------------------------------------------------------- 19# operator classes 20 21class VIEW3D_OT_materialutilities_assign_material_edit(bpy.types.Operator): 22 """Assign a material to the current selection""" 23 24 bl_idname = "view3d.materialutilities_assign_material_edit" 25 bl_label = "Assign Material (Material Utilities)" 26 bl_options = {'REGISTER', 'UNDO'} 27 28 material_name: StringProperty( 29 name = 'Material Name', 30 description = 'Name of Material to assign to current selection', 31 default = "", 32 maxlen = 63 33 ) 34 new_material: BoolProperty( 35 name = '', 36 description = 'Add a new material, enter the name in the box', 37 default = False 38 ) 39 show_dialog: BoolProperty( 40 name = 'Show Dialog', 41 default = False 42 ) 43 44 @classmethod 45 def poll(cls, context): 46 return context.active_object is not None 47 48 def invoke(self, context, event): 49 if self.show_dialog: 50 return context.window_manager.invoke_props_dialog(self) 51 else: 52 return self.execute(context) 53 54 def draw(self, context): 55 layout = self.layout 56 57 col = layout.column() 58 row = col.split(factor = 0.9, align = True) 59 60 if self.new_material: 61 row.prop(self, "material_name") 62 else: 63 row.prop_search(self, "material_name", bpy.data, "materials") 64 65 row.prop(self, "new_material", expand = True, icon = 'ADD') 66 67 def execute(self, context): 68 material_name = self.material_name 69 70 if self.new_material: 71 material_name = mu_new_material_name(material_name) 72 elif material_name == "": 73 self.report({'WARNING'}, "No Material Name given!") 74 return {'CANCELLED'} 75 76 return mu_assign_material(self, material_name, 'APPEND_MATERIAL') 77 78 79class VIEW3D_OT_materialutilities_assign_material_object(bpy.types.Operator): 80 """Assign a material to the current selection 81 (See the operator panel [F9] for more options)""" 82 83 bl_idname = "view3d.materialutilities_assign_material_object" 84 bl_label = "Assign Material (Material Utilities)" 85 bl_options = {'REGISTER', 'UNDO'} 86 87 material_name: StringProperty( 88 name = 'Material Name', 89 description = 'Name of Material to assign to current selection', 90 default = "", 91 maxlen = 63 92 ) 93 override_type: EnumProperty( 94 name = 'Assignment method', 95 description = '', 96 items = mu_override_type_enums 97 ) 98 new_material: BoolProperty( 99 name = '', 100 description = 'Add a new material, enter the name in the box', 101 default = False 102 ) 103 show_dialog: BoolProperty( 104 name = 'Show Dialog', 105 default = False 106 ) 107 108 @classmethod 109 def poll(cls, context): 110 return len(context.selected_editable_objects) > 0 111 112 def invoke(self, context, event): 113 if self.show_dialog: 114 return context.window_manager.invoke_props_dialog(self) 115 else: 116 return self.execute(context) 117 118 def draw(self, context): 119 layout = self.layout 120 121 col = layout.column() 122 row = col.split(factor=0.9, align = True) 123 124 if self.new_material: 125 row.prop(self, "material_name") 126 else: 127 row.prop_search(self, "material_name", bpy.data, "materials") 128 129 row.prop(self, "new_material", expand = True, icon = 'ADD') 130 131 layout.prop(self, "override_type") 132 133 134 def execute(self, context): 135 material_name = self.material_name 136 override_type = self.override_type 137 138 if self.new_material: 139 material_name = mu_new_material_name(material_name) 140 elif material_name == "": 141 self.report({'WARNING'}, "No Material Name given!") 142 return {'CANCELLED'} 143 144 result = mu_assign_material(self, material_name, override_type) 145 return result 146 147class VIEW3D_OT_materialutilities_select_by_material_name(bpy.types.Operator): 148 """Select geometry that has the chosen material assigned to it 149 (See the operator panel [F9] for more options)""" 150 151 bl_idname = "view3d.materialutilities_select_by_material_name" 152 bl_label = "Select By Material Name (Material Utilities)" 153 bl_options = {'REGISTER', 'UNDO'} 154 155 extend_selection: BoolProperty( 156 name = 'Extend Selection', 157 description = 'Keeps the current selection and adds faces with the material to the selection' 158 ) 159 material_name: StringProperty( 160 name = 'Material Name', 161 description = 'Name of Material to find and Select', 162 maxlen = 63 163 ) 164 show_dialog: BoolProperty( 165 name = 'Show Dialog', 166 default = False 167 ) 168 169 @classmethod 170 def poll(cls, context): 171 return len(context.visible_objects) > 0 172 173 def invoke(self, context, event): 174 if self.show_dialog: 175 return context.window_manager.invoke_props_dialog(self) 176 else: 177 return self.execute(context) 178 179 def draw(self, context): 180 layout = self.layout 181 layout.prop_search(self, "material_name", bpy.data, "materials") 182 183 layout.prop(self, "extend_selection", icon = "SELECT_EXTEND") 184 185 def execute(self, context): 186 material_name = self.material_name 187 ext = self.extend_selection 188 return mu_select_by_material_name(self, material_name, ext) 189 190 191class VIEW3D_OT_materialutilities_copy_material_to_others(bpy.types.Operator): 192 """Copy the material(s) of the active object to the other selected objects""" 193 194 bl_idname = "view3d.materialutilities_copy_material_to_others" 195 bl_label = "Copy material(s) to others (Material Utilities)" 196 bl_options = {'REGISTER', 'UNDO'} 197 198 @classmethod 199 def poll(cls, context): 200 return (context.active_object is not None) and (context.active_object.mode != 'EDIT') 201 202 def execute(self, context): 203 return mu_copy_material_to_others(self) 204 205 206class VIEW3D_OT_materialutilities_clean_material_slots(bpy.types.Operator): 207 """Removes any material slots from the selected objects that are not used""" 208 209 bl_idname = "view3d.materialutilities_clean_material_slots" 210 bl_label = "Clean Material Slots (Material Utilities)" 211 bl_options = {'REGISTER', 'UNDO'} 212 213 # affect: EnumProperty( 214 # name = "Affect", 215 # description = "Which objects material slots should be cleaned", 216 # items = mu_clean_slots_enums, 217 # default = 'ACTIVE' 218 # ) 219 220 only_active: BoolProperty( 221 name = 'Only active object', 222 description = 'Only remove the material slots for the active object ' + 223 '(otherwise do it for every selected object)', 224 default = True 225 ) 226 227 @classmethod 228 def poll(cls, context): 229 return len(context.selected_editable_objects) > 0 230 231 def draw(self, context): 232 layout = self.layout 233 layout.prop(self, "only_active", icon = "PIVOT_ACTIVE") 234 235 def execute(self, context): 236 affect = "ACTIVE" if self.only_active else "SELECTED" 237 238 return mu_cleanmatslots(self, affect) 239 240 241class VIEW3D_OT_materialutilities_remove_material_slot(bpy.types.Operator): 242 """Remove the active material slot from selected object(s) 243 (See the operator panel [F9] for more options)""" 244 245 bl_idname = "view3d.materialutilities_remove_material_slot" 246 bl_label = "Remove Active Material Slot (Material Utilities)" 247 bl_options = {'REGISTER', 'UNDO'} 248 249 only_active: BoolProperty( 250 name = 'Only active object', 251 description = 'Only remove the active material slot for the active object ' + 252 '(otherwise do it for every selected object)', 253 default = True 254 ) 255 256 @classmethod 257 def poll(cls, context): 258 return (context.active_object is not None) and (context.active_object.mode != 'EDIT') 259 260 def draw(self, context): 261 layout = self.layout 262 layout.prop(self, "only_active", icon = "PIVOT_ACTIVE") 263 264 def execute(self, context): 265 return mu_remove_material(self, self.only_active) 266 267class VIEW3D_OT_materialutilities_remove_all_material_slots(bpy.types.Operator): 268 """Remove all material slots from selected object(s) 269 (See the operator panel [F9] for more options)""" 270 271 bl_idname = "view3d.materialutilities_remove_all_material_slots" 272 bl_label = "Remove All Material Slots (Material Utilities)" 273 bl_options = {'REGISTER', 'UNDO'} 274 275 only_active: BoolProperty( 276 name = 'Only active object', 277 description = 'Only remove the material slots for the active object ' + 278 '(otherwise do it for every selected object)', 279 default = True 280 ) 281 282 @classmethod 283 def poll(cls, context): 284 return (context.active_object is not None) and (context.active_object.mode != 'EDIT') 285 286 def draw(self, context): 287 layout = self.layout 288 layout.prop(self, "only_active", icon = "PIVOT_ACTIVE") 289 290 def execute(self, context): 291 return mu_remove_all_materials(self, self.only_active) 292 293 294class VIEW3D_OT_materialutilities_replace_material(bpy.types.Operator): 295 """Replace a material by name""" 296 bl_idname = "view3d.materialutilities_replace_material" 297 bl_label = "Replace Material (Material Utilities)" 298 bl_options = {'REGISTER', 'UNDO'} 299 300 matorg: StringProperty( 301 name = "Original", 302 description = "Material to find and replace", 303 maxlen = 63, 304 ) 305 matrep: StringProperty(name="Replacement", 306 description = "Material that will be used instead of the Original material", 307 maxlen = 63, 308 ) 309 all_objects: BoolProperty( 310 name = "All Objects", 311 description = "Replace for all objects in this blend file (otherwise only selected objects)", 312 default = True, 313 ) 314 update_selection: BoolProperty( 315 name = "Update Selection", 316 description = "Select affected objects and deselect unaffected", 317 default = True, 318 ) 319 320 def draw(self, context): 321 layout = self.layout 322 323 layout.prop_search(self, "matorg", bpy.data, "materials") 324 layout.prop_search(self, "matrep", bpy.data, "materials") 325 layout.separator() 326 327 layout.prop(self, "all_objects", icon = "BLANK1") 328 layout.prop(self, "update_selection", icon = "SELECT_INTERSECT") 329 330 def invoke(self, context, event): 331 return context.window_manager.invoke_props_dialog(self) 332 333 def execute(self, context): 334 return mu_replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection) 335 336 337class VIEW3D_OT_materialutilities_fake_user_set(bpy.types.Operator): 338 """Enable/disable fake user for materials""" 339 340 bl_idname = "view3d.materialutilities_fake_user_set" 341 bl_label = "Set Fake User (Material Utilities)" 342 bl_options = {'REGISTER', 'UNDO'} 343 344 fake_user: EnumProperty( 345 name = "Fake User", 346 description = "Turn fake user on or off", 347 items = mu_fake_user_set_enums, 348 default = 'TOGGLE' 349 ) 350 351 affect: EnumProperty( 352 name = "Affect", 353 description = "Which materials of objects to affect", 354 items = mu_fake_user_affect_enums, 355 default = 'UNUSED' 356 ) 357 358 @classmethod 359 def poll(cls, context): 360 return (context.active_object is not None) 361 362 def draw(self, context): 363 layout = self.layout 364 layout.prop(self, "fake_user", expand = True) 365 layout.separator() 366 367 layout.prop(self, "affect") 368 369 def invoke(self, context, event): 370 return context.window_manager.invoke_props_dialog(self) 371 372 def execute(self, context): 373 return mu_set_fake_user(self, self.fake_user, self.affect) 374 375 376class VIEW3D_OT_materialutilities_change_material_link(bpy.types.Operator): 377 """Link the materials to Data or Object, while keepng materials assigned""" 378 379 bl_idname = "view3d.materialutilities_change_material_link" 380 bl_label = "Change Material Linking (Material Utilities)" 381 bl_options = {'REGISTER', 'UNDO'} 382 383 override: BoolProperty( 384 name = "Override Data material", 385 description = "Override the materials assigned to the object data/mesh when switching to 'Linked to Data'\n" + 386 "(WARNING: This will override the materials of other linked objects, " + 387 "which have the materials linked to Data)", 388 default = False, 389 ) 390 link_to: EnumProperty( 391 name = "Link", 392 description = "What should the material be linked to", 393 items = mu_link_to_enums, 394 default = 'OBJECT' 395 ) 396 397 affect: EnumProperty( 398 name = "Affect", 399 description = "Which materials of objects to affect", 400 items = mu_link_affect_enums, 401 default = 'SELECTED' 402 ) 403 404 @classmethod 405 def poll(cls, context): 406 return (context.active_object is not None) 407 408 def draw(self, context): 409 layout = self.layout 410 411 layout.prop(self, "link_to", expand = True) 412 layout.separator() 413 414 layout.prop(self, "affect") 415 layout.separator() 416 417 layout.prop(self, "override", icon = "DECORATE_OVERRIDE") 418 419 def invoke(self, context, event): 420 return context.window_manager.invoke_props_dialog(self) 421 422 def execute(self, context): 423 return mu_change_material_link(self, self.link_to, self.affect, self.override) 424 425class MATERIAL_OT_materialutilities_merge_base_names(bpy.types.Operator): 426 """Merges materials that has the same base names but ends with .xxx (.001, .002 etc)""" 427 428 bl_idname = "material.materialutilities_merge_base_names" 429 bl_label = "Merge Base Names" 430 bl_description = "Merge materials that has the same base names but ends with .xxx (.001, .002 etc)" 431 432 material_base_name: StringProperty( 433 name = "Material Base Name", 434 default = "", 435 description = 'Base name for materials to merge ' + 436 '(e.g. "Material" is the base name of "Material.001", "Material.002" etc.)' 437 ) 438 is_auto: BoolProperty( 439 name = "Auto Merge", 440 description = "Find all available duplicate materials and Merge them" 441 ) 442 443 is_not_undo = False 444 material_error = [] # collect mat for warning messages 445 446 447 def replace_name(self): 448 """If the user chooses a material like 'Material.042', clean it up to get a base name ('Material')""" 449 450 # use the chosen material as a base one, check if there is a name 451 self.check_no_name = (False if self.material_base_name in {""} else True) 452 453 # No need to do this if it's already "clean" 454 # (Also lessens the potential of error given about the material with the Base name) 455 if '.' not in self.material_base_name: 456 return 457 458 if self.check_no_name is True: 459 for mat in bpy.data.materials: 460 name = mat.name 461 462 if name == self.material_base_name: 463 try: 464 base, suffix = name.rsplit('.', 1) 465 466 # trigger the exception 467 num = int(suffix, 10) 468 self.material_base_name = base 469 mat.name = self.material_base_name 470 return 471 except ValueError: 472 if name not in self.material_error: 473 self.material_error.append(name) 474 return 475 476 return 477 478 def split_name(self, material): 479 """Split the material name into a base and a suffix""" 480 481 name = material.name 482 483 # No need to do this if it's already "clean"/there is no suffix 484 if '.' not in name: 485 return name, None 486 487 base, suffix = name.rsplit('.', 1) 488 489 try: 490 # trigger the exception 491 num = int(suffix, 10) 492 except ValueError: 493 # Not a numeric suffix 494 # Don't report on materials not actually included in the merge! 495 if ((self.is_auto or base == self.material_base_name) 496 and (name not in self.material_error)): 497 self.material_error.append(name) 498 return name, None 499 500 if self.is_auto is False: 501 if base == self.material_base_name: 502 return base, suffix 503 else: 504 return name, None 505 506 return base, suffix 507 508 def fixup_slot(self, slot): 509 """Fix material slots that was assigned to materials now removed""" 510 511 if not slot.material: 512 return 513 514 base, suffix = self.split_name(slot.material) 515 if suffix is None: 516 return 517 518 try: 519 base_mat = bpy.data.materials[base] 520 except KeyError: 521 print("\n[Materials Utilities Specials]\nLink to base names\nError:" 522 "Base material %r not found\n" % base) 523 return 524 525 slot.material = base_mat 526 527 def main_loop(self, context): 528 """Loops through all objects and material slots to make sure they are assigned to the right material""" 529 530 for obj in context.scene.objects: 531 for slot in obj.material_slots: 532 self.fixup_slot(slot) 533 534 @classmethod 535 def poll(self, context): 536 return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0) 537 538 def draw(self, context): 539 layout = self.layout 540 541 box_1 = layout.box() 542 box_1.prop_search(self, "material_base_name", bpy.data, "materials") 543 box_1.enabled = not self.is_auto 544 layout.separator() 545 546 layout.prop(self, "is_auto", text = "Auto Rename/Replace", icon = "SYNTAX_ON") 547 548 def invoke(self, context, event): 549 self.is_not_undo = True 550 return context.window_manager.invoke_props_dialog(self) 551 552 def execute(self, context): 553 # Reset Material errors, otherwise we risk reporting errors erroneously.. 554 self.material_error = [] 555 556 if not self.is_auto: 557 self.replace_name() 558 559 if self.check_no_name: 560 self.main_loop(context) 561 else: 562 self.report({'WARNING'}, "No Material Base Name given!") 563 564 self.is_not_undo = False 565 return {'CANCELLED'} 566 567 self.main_loop(context) 568 569 if self.material_error: 570 materials = ", ".join(self.material_error) 571 572 if len(self.material_error) == 1: 573 waswere = " was" 574 suff_s = "" 575 else: 576 waswere = " were" 577 suff_s = "s" 578 579 self.report({'WARNING'}, materials + waswere + " not removed or set as Base" + suff_s) 580 581 self.is_not_undo = False 582 return {'FINISHED'} 583 584class MATERIAL_OT_materialutilities_material_slot_move(bpy.types.Operator): 585 """Move the active material slot""" 586 587 bl_idname = "material.materialutilities_slot_move" 588 bl_label = "Move Slot" 589 bl_description = "Move the material slot" 590 bl_options = {'REGISTER', 'UNDO'} 591 592 movement: EnumProperty( 593 name = "Move", 594 description = "How to move the material slot", 595 items = mu_material_slot_move_enums 596 ) 597 598 @classmethod 599 def poll(self, context): 600 # would prefer to access self.movement here, but can't.. 601 obj = context.active_object 602 if not obj: 603 return False 604 if (obj.active_material_index < 0) or (len(obj.material_slots) <= 1): 605 return False 606 return True 607 608 def execute(self, context): 609 active_object = context.active_object 610 active_material = context.object.active_material 611 612 if self.movement == 'TOP': 613 dir = 'UP' 614 615 steps = active_object.active_material_index 616 else: 617 dir = 'DOWN' 618 619 last_slot_index = len(active_object.material_slots) - 1 620 steps = last_slot_index - active_object.active_material_index 621 622 if steps == 0: 623 self.report({'WARNING'}, active_material.name + " already at " + self.movement.lower() + '!') 624 else: 625 for i in range(steps): 626 bpy.ops.object.material_slot_move(direction = dir) 627 628 self.report({'INFO'}, active_material.name + ' moved to ' + self.movement.lower()) 629 630 return {'FINISHED'} 631 632 633 634class MATERIAL_OT_materialutilities_join_objects(bpy.types.Operator): 635 """Join objects that have the same (selected) material(s)""" 636 637 bl_idname = "material.materialutilities_join_objects" 638 bl_label = "Join by material (Material Utilities)" 639 bl_description = "Join objects that share the same material" 640 bl_options = {'REGISTER', 'UNDO'} 641 642 material_name: StringProperty( 643 name = "Material", 644 default = "", 645 description = 'Material to use to join objects' 646 ) 647 is_auto: BoolProperty( 648 name = "Auto Join", 649 description = "Join objects for all materials" 650 ) 651 652 is_not_undo = True 653 material_error = [] # collect mat for warning messages 654 655 656 @classmethod 657 def poll(self, context): 658 # This operator only works in Object mode 659 return (context.mode == 'OBJECT') and (len(context.visible_objects) > 0) 660 661 def draw(self, context): 662 layout = self.layout 663 664 box_1 = layout.box() 665 box_1.prop_search(self, "material_name", bpy.data, "materials") 666 box_1.enabled = not self.is_auto 667 layout.separator() 668 669 layout.prop(self, "is_auto", text = "Auto Join", icon = "SYNTAX_ON") 670 671 def invoke(self, context, event): 672 self.is_not_undo = True 673 return context.window_manager.invoke_props_dialog(self) 674 675 def execute(self, context): 676 # Reset Material errors, otherwise we risk reporting errors erroneously.. 677 self.material_error = [] 678 materials = [] 679 680 if not self.is_auto: 681 if self.material_name == "": 682 self.report({'WARNING'}, "No Material Name given!") 683 684 self.is_not_undo = False 685 return {'CANCELLED'} 686 materials = [self.material_name] 687 else: 688 materials = bpy.data.materials.keys() 689 690 result = mu_join_objects(self, materials) 691 self.is_not_undo = False 692 693 return result 694 695 696class MATERIAL_OT_materialutilities_auto_smooth_angle(bpy.types.Operator): 697 """Set Auto smooth values for selected objects""" 698 # Inspired by colkai 699 700 bl_idname = "view3d.materialutilities_auto_smooth_angle" 701 bl_label = "Set Auto Smooth Angle (Material Utilities)" 702 bl_options = {'REGISTER', 'UNDO'} 703 704 affect: EnumProperty( 705 name = "Affect", 706 description = "Which objects of to affect", 707 items = mu_affect_enums, 708 default = 'SELECTED' 709 ) 710 angle: FloatProperty( 711 name = "Angle", 712 description = "Maximum angle between face normals that will be considered as smooth", 713 subtype = 'ANGLE', 714 min = 0, 715 max = radians(180), 716 default = radians(35) 717 ) 718 set_smooth_shading: BoolProperty( 719 name = "Set Smooth", 720 description = "Set Smooth shading for the affected objects\n" 721 "This overrides the currenth smooth/flat shading that might be set to different parts of the object", 722 default = True 723 ) 724 725 @classmethod 726 def poll(cls, context): 727 return (len(bpy.data.objects) > 0) and (context.mode == 'OBJECT') 728 729 def invoke(self, context, event): 730 self.is_not_undo = True 731 return context.window_manager.invoke_props_dialog(self) 732 733 def draw(self, context): 734 layout = self.layout 735 736 layout.prop(self, "angle") 737 layout.prop(self, "affect") 738 739 layout.prop(self, "set_smooth_shading", icon = "BLANK1") 740 741 def execute(self, context): 742 return mu_set_auto_smooth(self, self.angle, self.affect, self.set_smooth_shading) 743