Merge branch 'blender-v3.6-release'
[blender-addons.git] / materials_utils / functions.py
blobe771fc6b1581ea02139970c70c85bf1977a65f03
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 from math import radians, degrees
6 # -----------------------------------------------------------------------------
7 # utility functions
9 def mu_assign_material_slots(object, material_list):
10 """Given an object and a list of material names removes all material slots from the object
11 adds new ones for each material in the material list, adds the materials to the slots as well."""
13 scene = bpy.context.scene
14 active_object = bpy.context.active_object
15 bpy.context.view_layer.objects.active = object
17 for s in object.material_slots:
18 bpy.ops.object.material_slot_remove()
20 # re-add them and assign material
21 i = 0
22 for mat in material_list:
23 material = bpy.data.materials[mat]
24 object.data.materials.append(material)
25 i += 1
27 # restore active object:
28 bpy.context.view_layer.objects.active = active_object
30 def mu_assign_to_data(object, material, index, edit_mode, all = True):
31 """Assign the material to the object data (polygons/splines)"""
33 if object.type == 'MESH':
34 # now assign the material to the mesh
35 mesh = object.data
36 if all:
37 for poly in mesh.polygons:
38 poly.material_index = index
39 else:
40 for poly in mesh.polygons:
41 if poly.select:
42 poly.material_index = index
44 mesh.update()
46 elif object.type in {'CURVE', 'SURFACE', 'TEXT'}:
47 bpy.ops.object.mode_set(mode = 'EDIT') # This only works in Edit mode
49 # If operator was run in Object mode
50 if not edit_mode:
51 # Select everything in Edit mode
52 bpy.ops.curve.select_all(action = 'SELECT')
54 bpy.ops.object.material_slot_assign() # Assign material of the current slot to selection
56 if not edit_mode:
57 bpy.ops.object.mode_set(mode = 'OBJECT')
59 def mu_new_material_name(material):
60 for mat in bpy.data.materials:
61 name = mat.name
63 if (name == material):
64 try:
65 base, suffix = name.rsplit('.', 1)
67 # trigger the exception
68 num = int(suffix, 10)
69 material = base + "." + '%03d' % (num + 1)
70 except ValueError:
71 material = material + ".001"
73 return material
76 def mu_clear_materials(object):
77 #obj.data.materials.clear()
79 for mat in object.material_slots:
80 bpy.ops.object.material_slot_remove()
83 def mu_assign_material(self, material_name = "Default", override_type = 'APPEND_MATERIAL', link_override = 'KEEP'):
84 """Assign the defined material to selected polygons/objects"""
86 # get active object so we can restore it later
87 active_object = bpy.context.active_object
89 edit_mode = False
90 all_polygons = True
91 if (not active_object is None) and active_object.mode == 'EDIT':
92 edit_mode = True
93 all_polygons = False
94 bpy.ops.object.mode_set()
96 # check if material exists, if it doesn't then create it
97 found = False
98 for material in bpy.data.materials:
99 if material.name == material_name:
100 target = material
101 found = True
102 break
104 if not found:
105 target = bpy.data.materials.new(mu_new_material_name(material_name))
106 target.use_nodes = True # When do we not want nodes today?
109 index = 0
110 objects = bpy.context.selected_editable_objects
112 for obj in objects:
113 # Apparently selected_editable_objects includes objects as cameras etc
114 if not obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
115 continue
117 # set the active object to our object
118 scene = bpy.context.scene
119 bpy.context.view_layer.objects.active = obj
121 if link_override == 'KEEP':
122 if len(obj.material_slots) > 0:
123 link = obj.material_slots[0].link
124 else:
125 link = 'DATA'
126 else:
127 link = link_override
129 # If we should override all current material slots
130 if override_type == 'OVERRIDE_ALL' or obj.type == 'META':
132 # If there's more than one slot, Clear out all the material slots
133 if len(obj.material_slots) > 1:
134 mu_clear_materials(obj)
136 # If there's no slots left/never was one, add a slot
137 if len(obj.material_slots) == 0:
138 bpy.ops.object.material_slot_add()
140 # Assign the material to that slot
141 obj.material_slots[0].link = link
142 obj.material_slots[0].material = target
144 if obj.type == 'META':
145 self.report({'INFO'}, "Meta balls only support one material, all other materials overridden!")
147 # If we should override each material slot
148 elif override_type == 'OVERRIDE_SLOTS':
149 i = 0
150 # go through each slot
151 for material in obj.material_slots:
152 # assign the target material to current slot
153 if not link_override == 'KEEP':
154 obj.material_slots[i].link = link
155 obj.material_slots[i].material = target
156 i += 1
158 elif override_type == 'OVERRIDE_CURRENT':
159 active_slot = obj.active_material_index
161 if len(obj.material_slots) == 0:
162 self.report({'INFO'}, 'No material slots found! A material slot was added!')
163 bpy.ops.object.material_slot_add()
165 obj.material_slots[active_slot].material = target
167 # if we should keep the material slots and just append the selected material (if not already assigned)
168 elif override_type == 'APPEND_MATERIAL':
169 found = False
170 i = 0
171 material_slots = obj.material_slots
173 if (obj.data.users > 1) and (len(material_slots) >= 1 and material_slots[0].link == 'OBJECT'):
174 self.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' +
175 'Unwanted results might happen!')
177 # check material slots for material_name materia
178 for material in material_slots:
179 if material.name == material_name:
180 found = True
181 index = i
183 # make slot active
184 obj.active_material_index = i
185 break
186 i += 1
188 if not found:
189 # In Edit mode, or if there's not a slot, append the assigned material
190 # If we're overriding, there's currently no materials at all, so after this there will be 1
191 # If not, this adds another slot with the assigned material
193 index = len(obj.material_slots)
194 bpy.ops.object.material_slot_add()
195 obj.material_slots[index].link = link
196 obj.material_slots[index].material = target
197 obj.active_material_index = index
199 mu_assign_to_data(obj, target, index, edit_mode, all_polygons)
201 # We shouldn't risk unsetting the active object
202 if not active_object is None:
203 # restore the active object
204 bpy.context.view_layer.objects.active = active_object
206 if edit_mode:
207 bpy.ops.object.mode_set(mode='EDIT')
209 return {'FINISHED'}
212 def mu_select_by_material_name(self, find_material_name, extend_selection = False, internal = False):
213 """Searches through all objects, or the polygons/curves of the current object
214 to find and select objects/data with the desired material"""
216 # in object mode selects all objects with material find_material_name
217 # in edit mode selects all polygons with material find_material_name
219 find_material = bpy.data.materials.get(find_material_name)
221 if find_material is None:
222 self.report({'INFO'}, "The material " + find_material_name + " doesn't exists!")
223 return {'CANCELLED'} if not internal else -1
225 # check for edit_mode
226 edit_mode = False
227 found_material = False
229 scene = bpy.context.scene
231 # set selection mode to polygons
232 scene.tool_settings.mesh_select_mode = False, False, True
234 active_object = bpy.context.active_object
236 if (not active_object is None) and (active_object.mode == 'EDIT'):
237 edit_mode = True
239 if not edit_mode:
240 objects = bpy.context.visible_objects
242 for obj in objects:
243 if obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
244 mat_slots = obj.material_slots
245 for material in mat_slots:
246 if material.material == find_material:
247 obj.select_set(state = True)
249 found_material = True
251 # the active object may not have the material!
252 # set it to one that does!
253 bpy.context.view_layer.objects.active = obj
254 break
255 else:
256 if not extend_selection:
257 obj.select_set(state=False)
259 #deselect non-meshes
260 elif not extend_selection:
261 obj.select_set(state=False)
263 if not found_material:
264 if not internal:
265 self.report({'INFO'}, "No objects found with the material " +
266 find_material_name + "!")
267 return {'FINISHED'} if not internal else 0
269 else:
270 # it's edit_mode, so select the polygons
272 if active_object.type == 'MESH':
273 # if not extending the selection, deselect all first
274 # (Without this, edges/faces were still selected
275 # while the faces were deselected)
276 if not extend_selection:
277 bpy.ops.mesh.select_all(action = 'DESELECT')
279 objects = bpy.context.selected_editable_objects
281 for obj in objects:
282 bpy.context.view_layer.objects.active = obj
284 if obj.type == 'MESH':
285 bpy.ops.object.mode_set()
287 mat_slots = obj.material_slots
289 # same material can be on multiple slots
290 slot_indeces = []
291 i = 0
292 for material in mat_slots:
293 if material.material == find_material:
294 slot_indeces.append(i)
295 i += 1
297 mesh = obj.data
299 for poly in mesh.polygons:
300 if poly.material_index in slot_indeces:
301 poly.select = True
302 found_material = True
303 elif not extend_selection:
304 poly.select = False
306 mesh.update()
308 bpy.ops.object.mode_set(mode = 'EDIT')
311 elif obj.type in {'CURVE', 'SURFACE'}:
312 # For Curve objects, there can only be one material per spline
313 # and thus each spline is linked to one material slot.
314 # So to not have to care for different data structures
315 # for different curve types, we use the material slots
316 # and the built in selection methods
317 # (Technically, this should work for meshes as well)
319 mat_slots = obj.material_slots
321 i = 0
322 for material in mat_slots:
323 bpy.context.active_object.active_material_index = i
325 if material.material == find_material:
326 bpy.ops.object.material_slot_select()
327 found_material = True
328 elif not extend_selection:
329 bpy.ops.object.material_slot_deselect()
331 i += 1
333 elif not internal:
334 # Some object types are not supported
335 # mostly because don't really support selecting by material (like Font/Text objects)
336 # or that they don't support multiple materials/are just "weird" (i.e. Meta balls)
337 self.report({'WARNING'}, "The type '" +
338 obj.type +
339 "' isn't supported in Edit mode by Material Utilities!")
340 #return {'CANCELLED'}
342 bpy.context.view_layer.objects.active = active_object
344 if (not found_material) and (not internal):
345 self.report({'INFO'}, "Material " + find_material_name + " isn't assigned to anything!")
347 return {'FINISHED'} if not internal else 1
350 def mu_copy_material_to_others(self):
351 """Copy the material to of the current object to the other seleceted all_objects"""
352 # Currently uses the built-in method
353 # This could be extended to work in edit mode as well
355 #active_object = context.active_object
357 bpy.ops.object.material_slot_copy()
359 return {'FINISHED'}
362 def mu_cleanmatslots(self, affect):
363 """Clean the material slots of the seleceted objects"""
365 # check for edit mode
366 edit_mode = False
367 active_object = bpy.context.active_object
368 if active_object.mode == 'EDIT':
369 edit_mode = True
370 bpy.ops.object.mode_set()
372 objects = []
374 if affect == 'ACTIVE':
375 objects = [active_object]
376 elif affect == 'SELECTED':
377 objects = bpy.context.selected_editable_objects
378 elif affect == 'SCENE':
379 objects = bpy.context.scene.objects
380 else: # affect == 'ALL'
381 objects = bpy.data.objects
383 for obj in objects:
384 used_mat_index = [] # we'll store used materials indices here
385 assigned_materials = []
386 material_list = []
387 material_names = []
389 materials = obj.material_slots.keys()
391 if obj.type == 'MESH':
392 # check the polygons on the mesh to build a list of used materials
393 mesh = obj.data
395 for poly in mesh.polygons:
396 # get the material index for this face...
397 material_index = poly.material_index
399 if material_index >= len(materials):
400 poly.select = True
401 self.report({'ERROR'},
402 "A poly with an invalid material was found, this should not happen! Canceling!")
403 return {'CANCELLED'}
405 # indices will be lost: Store face mat use by name
406 current_mat = materials[material_index]
407 assigned_materials.append(current_mat)
409 # check if index is already listed as used or not
410 found = False
411 for mat in used_mat_index:
412 if mat == material_index:
413 found = True
415 if not found:
416 # add this index to the list
417 used_mat_index.append(material_index)
419 # re-assign the used materials to the mesh and leave out the unused
420 for u in used_mat_index:
421 material_list.append(materials[u])
422 # we'll need a list of names to get the face indices...
423 material_names.append(materials[u])
425 mu_assign_material_slots(obj, material_list)
427 # restore face indices:
428 i = 0
429 for poly in mesh.polygons:
430 material_index = material_names.index(assigned_materials[i])
431 poly.material_index = material_index
432 i += 1
434 elif obj.type in {'CURVE', 'SURFACE'}:
436 splines = obj.data.splines
438 for spline in splines:
439 # Get the material index of this spline
440 material_index = spline.material_index
442 # indices will be last: Store material use by name
443 current_mat = materials[material_index]
444 assigned_materials.append(current_mat)
446 # check if indek is already listed as used or not
447 found = False
448 for mat in used_mat_index:
449 if mat == material_index:
450 found = True
452 if not found:
453 # add this index to the list
454 used_mat_index.append(material_index)
456 # re-assigned the used materials to the curve and leave out the unused
457 for u in used_mat_index:
458 material_list.append(materials[u])
459 # we'll need a list of names to get the face indices
460 material_names.append(materials[u])
462 mu_assign_material_slots(obj, material_list)
464 # restore spline indices
465 i = 0
466 for spline in splines:
467 material_index = material_names.index(assigned_materials[i])
468 spline.material_index = material_index
469 i += 1
471 else:
472 # Some object types are not supported
473 self.report({'WARNING'},
474 "The type '" + obj.type + "' isn't currently supported " +
475 "for Material slots cleaning by Material Utilities!")
477 if edit_mode:
478 bpy.ops.object.mode_set(mode='EDIT')
480 return {'FINISHED'}
482 def mu_remove_material(self, for_active_object = False):
483 """Remove the active material slot from selected object(s)"""
485 if for_active_object:
486 bpy.ops.object.material_slot_remove()
487 else:
488 last_active = bpy.context.active_object
489 objects = bpy.context.selected_editable_objects
491 for obj in objects:
492 bpy.context.view_layer.objects.active = obj
493 bpy.ops.object.material_slot_remove()
495 bpy.context.view_layer.objects.active = last_active
497 return {'FINISHED'}
499 def mu_remove_all_materials(self, for_active_object = False):
500 """Remove all material slots from selected object(s)"""
502 if for_active_object:
503 obj = bpy.context.active_object
505 # Clear out the material slots
506 obj.data.materials.clear()
508 else:
509 last_active = bpy.context.active_object
510 objects = bpy.context.selected_editable_objects
512 for obj in objects:
513 obj.data.materials.clear()
515 bpy.context.view_layer.objects.active = last_active
517 return {'FINISHED'}
520 def mu_replace_material(material_a, material_b, all_objects=False, update_selection=False):
521 """Replace one material with another material"""
523 # material_a is the name of original material
524 # material_b is the name of the material to replace it with
525 # 'all' will replace throughout the blend file
527 mat_org = bpy.data.materials.get(material_a)
528 mat_rep = bpy.data.materials.get(material_b)
530 if mat_org != mat_rep and None not in (mat_org, mat_rep):
531 # Store active object
532 scn = bpy.context.scene
534 if all_objects:
535 objs = bpy.data.objects
536 else:
537 objs = bpy.context.selected_editable_objects
539 for obj in objs:
540 if obj.type == 'MESH':
541 match = False
543 for mat in obj.material_slots:
544 if mat.material == mat_org:
545 mat.material = mat_rep
547 # Indicate which objects were affected
548 if update_selection:
549 obj.select_set(state = True)
550 match = True
552 if update_selection and not match:
553 obj.select_set(state = False)
555 return {'FINISHED'}
558 def mu_set_fake_user(self, fake_user, materials):
559 """Set the fake user flag for the objects material"""
561 if materials == 'ALL':
562 mats = (mat for mat in bpy.data.materials if mat.library is None)
563 elif materials == 'UNUSED':
564 mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0)
565 else:
566 mats = []
567 if materials == 'ACTIVE':
568 objs = [bpy.context.active_object]
569 elif materials == 'SELECTED':
570 objs = bpy.context.selected_objects
571 elif materials == 'SCENE':
572 objs = bpy.context.scene.objects
573 else: # materials == 'USED'
574 objs = bpy.data.objects
575 # Maybe check for users > 0 instead?
577 mats = (mat for ob in objs
578 if hasattr(ob.data, "materials")
579 for mat in ob.data.materials
580 if mat.library is None)
582 if fake_user == 'TOGGLE':
583 done_mats = []
584 for mat in mats:
585 if not mat.name in done_mats:
586 mat.use_fake_user = not mat.use_fake_user
587 done_mats.append(mat.name)
588 else:
589 fake_user_val = fake_user == 'ON'
590 for mat in mats:
591 mat.use_fake_user = fake_user_val
593 for area in bpy.context.screen.areas:
594 if area.type in ('PROPERTIES', 'NODE_EDITOR'):
595 area.tag_redraw()
597 return {'FINISHED'}
600 def mu_change_material_link(self, link, affect, override_data_material = False):
601 """Change what the materials are linked to (Object or Data), while keeping materials assigned"""
603 objects = []
605 if affect == "ACTIVE":
606 objects = [bpy.context.active_object]
607 elif affect == "SELECTED":
608 objects = bpy.context.selected_objects
609 elif affect == "SCENE":
610 objects = bpy.context.scene.objects
611 elif affect == "ALL":
612 objects = bpy.data.objects
614 for object in objects:
615 index = 0
616 for slot in object.material_slots:
617 present_material = slot.material
619 if link == 'TOGGLE':
620 slot.link = ('DATA' if slot.link == 'OBJECT' else 'OBJECT')
621 else:
622 slot.link = link
624 if slot.link == 'OBJECT':
625 override_data_material = True
626 elif slot.material is None:
627 override_data_material = True
628 elif not override_data_material:
629 self.report({'INFO'},
630 'The object Data for object ' + object.name_full + ' already had a material assigned ' +
631 'to slot #' + str(index) + ' (' + slot.material.name + '), it was not overridden!')
633 if override_data_material:
634 slot.material = present_material
636 index = index + 1
638 return {'FINISHED'}
640 def mu_join_objects(self, materials):
641 """Join objects together based on their material"""
643 for material in materials:
644 mu_select_by_material_name(self, material, False, True)
646 bpy.ops.object.join()
648 return {'FINISHED'}
650 def mu_set_auto_smooth(self, angle, affect, set_smooth_shading):
651 """Set Auto smooth values for selected objects"""
652 # Inspired by colkai
654 objects = []
655 objects_affected = 0
657 if affect == "ACTIVE":
658 objects = [bpy.context.active_object]
659 elif affect == "SELECTED":
660 objects = bpy.context.selected_editable_objects
661 elif affect == "SCENE":
662 objects = bpy.context.scene.objects
663 elif affect == "ALL":
664 objects = bpy.data.objects
666 if len(objects) == 0:
667 self.report({'WARNING'}, 'No objects available to set Auto Smooth on')
668 return {'CANCELLED'}
670 for object in objects:
671 if object.type == "MESH":
672 if set_smooth_shading:
673 for poly in object.data.polygons:
674 poly.use_smooth = True
676 #bpy.ops.object.shade_smooth()
678 object.data.use_auto_smooth = 1
679 object.data.auto_smooth_angle = angle # 35 degrees as radians
681 objects_affected += 1
683 self.report({'INFO'}, 'Auto smooth angle set to %.0f° on %d of %d objects' %
684 (degrees(angle), objects_affected, len(objects)))
686 return {'FINISHED'}