1 # SPDX-License-Identifier: GPL-2.0-or-later
5 from bpy
.types
import Operator
6 from bpy
.props
import (
15 from .enum_values
import *
16 from .functions
import *
18 from math
import radians
20 # -----------------------------------------------------------------------------
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',
36 new_material
: BoolProperty(
38 description
= 'Add a new material, enter the name in the box',
41 show_dialog
: BoolProperty(
47 def poll(cls
, context
):
48 return context
.active_object
is not None
50 def invoke(self
, context
, event
):
52 return context
.window_manager
.invoke_props_dialog(self
)
54 return self
.execute(context
)
56 def draw(self
, context
):
60 row
= col
.split(factor
= 0.9, align
= True)
63 row
.prop(self
, "material_name")
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
73 material_name
= mu_new_material_name(material_name
)
74 elif material_name
== "":
75 self
.report({'WARNING'}, "No Material Name given!")
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',
95 override_type
: EnumProperty(
96 name
= 'Assignment method',
98 items
= mu_override_type_enums
100 new_material
: BoolProperty(
102 description
= 'Add a new material, enter the name in the box',
105 show_dialog
: BoolProperty(
106 name
= 'Show Dialog',
111 def poll(cls
, context
):
112 return len(context
.selected_editable_objects
) > 0
114 def invoke(self
, context
, event
):
116 return context
.window_manager
.invoke_props_dialog(self
)
118 return self
.execute(context
)
120 def draw(self
, context
):
123 col
= layout
.column()
124 row
= col
.split(factor
=0.9, align
= True)
126 if self
.new_material
:
127 row
.prop(self
, "material_name")
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!")
146 result
= mu_assign_material(self
, material_name
, override_type
)
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',
166 show_dialog
: BoolProperty(
167 name
= 'Show Dialog',
172 def poll(cls
, context
):
173 return len(context
.visible_objects
) > 0
175 def invoke(self
, context
, event
):
177 return context
.window_manager
.invoke_props_dialog(self
)
179 return self
.execute(context
)
181 def draw(self
, context
):
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'}
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(
217 # description = "Which objects material slots should be cleaned",
218 # items = mu_clean_slots_enums,
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)',
230 def poll(cls
, context
):
231 return len(context
.selected_editable_objects
) > 0
233 def draw(self
, context
):
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)',
259 def poll(cls
, context
):
260 return (context
.active_object
is not None) and (context
.active_object
.mode
!= 'EDIT')
262 def draw(self
, context
):
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)',
285 def poll(cls
, context
):
286 return (context
.active_object
is not None) and (context
.active_object
.mode
!= 'EDIT')
288 def draw(self
, context
):
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(
304 description
= "Material to find and replace",
307 matrep
: StringProperty(name
="Replacement",
308 description
= "Material that will be used instead of the Original material",
311 all_objects
: BoolProperty(
312 name
= "All Objects",
313 description
= "Replace for all objects in this blend file (otherwise only selected objects)",
316 update_selection
: BoolProperty(
317 name
= "Update Selection",
318 description
= "Select affected objects and deselect unaffected",
322 def draw(self
, context
):
325 layout
.prop_search(self
, "matorg", bpy
.data
, "materials")
326 layout
.prop_search(self
, "matrep", bpy
.data
, "materials")
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(
348 description
= "Turn fake user on or off",
349 items
= mu_fake_user_set_enums
,
353 affect
: EnumProperty(
355 description
= "Which materials of objects to affect",
356 items
= mu_fake_user_affect_enums
,
361 def poll(cls
, context
):
362 return (context
.active_object
is not None)
364 def draw(self
, context
):
366 layout
.prop(self
, "fake_user", expand
= True)
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)",
392 link_to
: EnumProperty(
394 description
= "What should the material be linked to",
395 items
= mu_link_to_enums
,
399 affect
: EnumProperty(
401 description
= "Which materials of objects to affect",
402 items
= mu_link_affect_enums
,
407 def poll(cls
, context
):
408 return (context
.active_object
is not None)
410 def draw(self
, context
):
413 layout
.prop(self
, "link_to", expand
= True)
416 layout
.prop(self
, "affect")
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",
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(
442 description
= "Find all available duplicate materials and Merge them"
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
:
460 if self
.check_no_name
is True:
461 for mat
in bpy
.data
.materials
:
464 if name
== self
.material_base_name
:
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
474 if name
not in self
.material_error
:
475 self
.material_error
.append(name
)
480 def split_name(self
, material
):
481 """Split the material name into a base and a suffix"""
485 # No need to do this if it's already "clean"/there is no suffix
489 base
, suffix
= name
.rsplit('.', 1)
492 # trigger the exception
493 num
= int(suffix
, 10)
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
)
502 if self
.is_auto
is False:
503 if base
== self
.material_base_name
:
510 def fixup_slot(self
, slot
):
511 """Fix material slots that was assigned to materials now removed"""
513 if not slot
.material
:
516 base
, suffix
= self
.split_name(slot
.material
)
521 base_mat
= bpy
.data
.materials
[base
]
523 print("\n[Materials Utilities Specials]\nLink to base names\nError:"
524 "Base material %r not found\n" % base
)
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
)
537 def poll(self
, context
):
538 return (context
.mode
== 'OBJECT') and (len(context
.visible_objects
) > 0)
540 def draw(self
, context
):
544 box_1
.prop_search(self
, "material_base_name", bpy
.data
, "materials")
545 box_1
.enabled
= not self
.is_auto
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
= []
561 if self
.check_no_name
:
562 self
.main_loop(context
)
564 self
.report({'WARNING'}, "No Material Base Name given!")
566 self
.is_not_undo
= False
569 self
.main_loop(context
)
571 if self
.material_error
:
572 materials
= ", ".join(self
.material_error
)
574 if len(self
.material_error
) == 1:
581 self
.report({'WARNING'}, materials
+ waswere
+ " not removed or set as Base" + suff_s
)
583 self
.is_not_undo
= False
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(
596 description
= "How to move the material slot",
597 items
= mu_material_slot_move_enums
601 def poll(self
, context
):
602 # would prefer to access self.movement here, but can't..
603 obj
= context
.active_object
606 if (obj
.active_material_index
< 0) or (len(obj
.material_slots
) <= 1):
610 def execute(self
, context
):
611 active_object
= context
.active_object
612 active_material
= context
.object.active_material
614 if self
.movement
== 'TOP':
617 steps
= active_object
.active_material_index
621 last_slot_index
= len(active_object
.material_slots
) - 1
622 steps
= last_slot_index
- active_object
.active_material_index
625 self
.report({'WARNING'}, active_material
.name
+ " already at " + self
.movement
.lower() + '!')
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())
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(
647 description
= 'Material to use to join objects'
649 is_auto
: BoolProperty(
651 description
= "Join objects for all materials"
655 material_error
= [] # collect mat for warning messages
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
):
667 box_1
.prop_search(self
, "material_name", bpy
.data
, "materials")
668 box_1
.enabled
= not self
.is_auto
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
= []
683 if self
.material_name
== "":
684 self
.report({'WARNING'}, "No Material Name given!")
686 self
.is_not_undo
= False
688 materials
= [self
.material_name
]
690 materials
= bpy
.data
.materials
.keys()
692 result
= mu_join_objects(self
, materials
)
693 self
.is_not_undo
= False
698 class MATERIAL_OT_materialutilities_auto_smooth_angle(bpy
.types
.Operator
):
699 """Set Auto smooth values for selected objects"""
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(
708 description
= "Which objects of to affect",
709 items
= mu_affect_enums
,
712 angle
: FloatProperty(
714 description
= "Maximum angle between face normals that will be considered as smooth",
718 default
= radians(35)
720 set_smooth_shading
: BoolProperty(
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",
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
):
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
)