1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from .enum_values
import *
18 from .functions
import *
20 from math
import radians
22 # -----------------------------------------------------------------------------
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',
38 new_material
: BoolProperty(
40 description
= 'Add a new material, enter the name in the box',
43 show_dialog
: BoolProperty(
49 def poll(cls
, context
):
50 return context
.active_object
is not None
52 def invoke(self
, context
, event
):
54 return context
.window_manager
.invoke_props_dialog(self
)
56 return self
.execute(context
)
58 def draw(self
, context
):
62 row
= col
.split(factor
= 0.9, align
= True)
65 row
.prop(self
, "material_name")
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
75 material_name
= mu_new_material_name(material_name
)
76 elif material_name
== "":
77 self
.report({'WARNING'}, "No Material Name given!")
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',
97 override_type
: EnumProperty(
98 name
= 'Assignment method',
100 items
= mu_override_type_enums
102 new_material
: BoolProperty(
104 description
= 'Add a new material, enter the name in the box',
107 show_dialog
: BoolProperty(
108 name
= 'Show Dialog',
113 def poll(cls
, context
):
114 return len(context
.selected_editable_objects
) > 0
116 def invoke(self
, context
, event
):
118 return context
.window_manager
.invoke_props_dialog(self
)
120 return self
.execute(context
)
122 def draw(self
, context
):
125 col
= layout
.column()
126 row
= col
.split(factor
=0.9, align
= True)
128 if self
.new_material
:
129 row
.prop(self
, "material_name")
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!")
148 result
= mu_assign_material(self
, material_name
, override_type
)
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',
168 show_dialog
: BoolProperty(
169 name
= 'Show Dialog',
174 def poll(cls
, context
):
175 return len(context
.visible_objects
) > 0
177 def invoke(self
, context
, event
):
179 return context
.window_manager
.invoke_props_dialog(self
)
181 return self
.execute(context
)
183 def draw(self
, context
):
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'}
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(
219 # description = "Which objects material slots should be cleaned",
220 # items = mu_clean_slots_enums,
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)',
232 def poll(cls
, context
):
233 return len(context
.selected_editable_objects
) > 0
235 def draw(self
, context
):
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)',
261 def poll(cls
, context
):
262 return (context
.active_object
is not None) and (context
.active_object
.mode
!= 'EDIT')
264 def draw(self
, context
):
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)',
287 def poll(cls
, context
):
288 return (context
.active_object
is not None) and (context
.active_object
.mode
!= 'EDIT')
290 def draw(self
, context
):
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(
306 description
= "Material to find and replace",
309 matrep
: StringProperty(name
="Replacement",
310 description
= "Material that will be used instead of the Original material",
313 all_objects
: BoolProperty(
314 name
= "All Objects",
315 description
= "Replace for all objects in this blend file (otherwise only selected objects)",
318 update_selection
: BoolProperty(
319 name
= "Update Selection",
320 description
= "Select affected objects and deselect unaffected",
324 def draw(self
, context
):
327 layout
.prop_search(self
, "matorg", bpy
.data
, "materials")
328 layout
.prop_search(self
, "matrep", bpy
.data
, "materials")
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(
350 description
= "Turn fake user on or off",
351 items
= mu_fake_user_set_enums
,
355 affect
: EnumProperty(
357 description
= "Which materials of objects to affect",
358 items
= mu_fake_user_affect_enums
,
363 def poll(cls
, context
):
364 return (context
.active_object
is not None)
366 def draw(self
, context
):
368 layout
.prop(self
, "fake_user", expand
= True)
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)",
394 link_to
: EnumProperty(
396 description
= "What should the material be linked to",
397 items
= mu_link_to_enums
,
401 affect
: EnumProperty(
403 description
= "Which materials of objects to affect",
404 items
= mu_link_affect_enums
,
409 def poll(cls
, context
):
410 return (context
.active_object
is not None)
412 def draw(self
, context
):
415 layout
.prop(self
, "link_to", expand
= True)
418 layout
.prop(self
, "affect")
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",
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(
444 description
= "Find all available duplicate materials and Merge them"
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
:
462 if self
.check_no_name
is True:
463 for mat
in bpy
.data
.materials
:
466 if name
== self
.material_base_name
:
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
476 if name
not in self
.material_error
:
477 self
.material_error
.append(name
)
482 def split_name(self
, material
):
483 """Split the material name into a base and a suffix"""
487 # No need to do this if it's already "clean"/there is no suffix
491 base
, suffix
= name
.rsplit('.', 1)
494 # trigger the exception
495 num
= int(suffix
, 10)
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
)
504 if self
.is_auto
is False:
505 if base
== self
.material_base_name
:
512 def fixup_slot(self
, slot
):
513 """Fix material slots that was assigned to materials now removed"""
515 if not slot
.material
:
518 base
, suffix
= self
.split_name(slot
.material
)
523 base_mat
= bpy
.data
.materials
[base
]
525 print("\n[Materials Utilities Specials]\nLink to base names\nError:"
526 "Base material %r not found\n" % base
)
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
)
539 def poll(self
, context
):
540 return (context
.mode
== 'OBJECT') and (len(context
.visible_objects
) > 0)
542 def draw(self
, context
):
546 box_1
.prop_search(self
, "material_base_name", bpy
.data
, "materials")
547 box_1
.enabled
= not self
.is_auto
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
= []
563 if self
.check_no_name
:
564 self
.main_loop(context
)
566 self
.report({'WARNING'}, "No Material Base Name given!")
568 self
.is_not_undo
= False
571 self
.main_loop(context
)
573 if self
.material_error
:
574 materials
= ", ".join(self
.material_error
)
576 if len(self
.material_error
) == 1:
583 self
.report({'WARNING'}, materials
+ waswere
+ " not removed or set as Base" + suff_s
)
585 self
.is_not_undo
= False
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(
598 description
= "How to move the material slot",
599 items
= mu_material_slot_move_enums
603 def poll(self
, context
):
604 # would prefer to access self.movement here, but can't..
605 obj
= context
.active_object
608 if (obj
.active_material_index
< 0) or (len(obj
.material_slots
) <= 1):
612 def execute(self
, context
):
613 active_object
= context
.active_object
614 active_material
= context
.object.active_material
616 if self
.movement
== 'TOP':
619 steps
= active_object
.active_material_index
623 last_slot_index
= len(active_object
.material_slots
) - 1
624 steps
= last_slot_index
- active_object
.active_material_index
627 self
.report({'WARNING'}, active_material
.name
+ " already at " + self
.movement
.lower() + '!')
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())
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(
649 description
= 'Material to use to join objects'
651 is_auto
: BoolProperty(
653 description
= "Join objects for all materials"
657 material_error
= [] # collect mat for warning messages
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
):
669 box_1
.prop_search(self
, "material_name", bpy
.data
, "materials")
670 box_1
.enabled
= not self
.is_auto
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
= []
685 if self
.material_name
== "":
686 self
.report({'WARNING'}, "No Material Name given!")
688 self
.is_not_undo
= False
690 materials
= [self
.material_name
]
692 materials
= bpy
.data
.materials
.keys()
694 result
= mu_join_objects(self
, materials
)
695 self
.is_not_undo
= False
700 class MATERIAL_OT_materialutilities_auto_smooth_angle(bpy
.types
.Operator
):
701 """Set Auto smooth values for selected objects"""
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(
710 description
= "Which objects of to affect",
711 items
= mu_affect_enums
,
714 angle
: FloatProperty(
716 description
= "Maximum angle between face normals that will be considered as smooth",
720 default
= radians(35)
722 set_smooth_shading
: BoolProperty(
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",
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
):
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
)