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