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