1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 from math
import radians
, degrees
8 # -----------------------------------------------------------------------------
11 def mu_assign_material_slots(object, material_list
):
12 """Given an object and a list of material names removes all material slots from the object
13 adds new ones for each material in the material list, adds the materials to the slots as well."""
15 scene
= bpy
.context
.scene
16 active_object
= bpy
.context
.active_object
17 bpy
.context
.view_layer
.objects
.active
= object
19 for s
in object.material_slots
:
20 bpy
.ops
.object.material_slot_remove()
22 # re-add them and assign material
24 for mat
in material_list
:
25 material
= bpy
.data
.materials
[mat
]
26 object.data
.materials
.append(material
)
29 # restore active object:
30 bpy
.context
.view_layer
.objects
.active
= active_object
32 def mu_assign_to_data(object, material
, index
, edit_mode
, all
= True):
33 """Assign the material to the object data (polygons/splines)"""
35 if object.type == 'MESH':
36 # now assign the material to the mesh
39 for poly
in mesh
.polygons
:
40 poly
.material_index
= index
42 for poly
in mesh
.polygons
:
44 poly
.material_index
= index
48 elif object.type in {'CURVE', 'SURFACE', 'TEXT'}:
49 bpy
.ops
.object.mode_set(mode
= 'EDIT') # This only works in Edit mode
51 # If operator was run in Object mode
53 # Select everything in Edit mode
54 bpy
.ops
.curve
.select_all(action
= 'SELECT')
56 bpy
.ops
.object.material_slot_assign() # Assign material of the current slot to selection
59 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
61 def mu_new_material_name(material
):
62 for mat
in bpy
.data
.materials
:
65 if (name
== material
):
67 base
, suffix
= name
.rsplit('.', 1)
69 # trigger the exception
71 material
= base
+ "." + '%03d' % (num
+ 1)
73 material
= material
+ ".001"
78 def mu_clear_materials(object):
79 #obj.data.materials.clear()
81 for mat
in object.material_slots
:
82 bpy
.ops
.object.material_slot_remove()
85 def mu_assign_material(self
, material_name
= "Default", override_type
= 'APPEND_MATERIAL', link_override
= 'KEEP'):
86 """Assign the defined material to selected polygons/objects"""
88 # get active object so we can restore it later
89 active_object
= bpy
.context
.active_object
93 if (not active_object
is None) and active_object
.mode
== 'EDIT':
96 bpy
.ops
.object.mode_set()
98 # check if material exists, if it doesn't then create it
100 for material
in bpy
.data
.materials
:
101 if material
.name
== material_name
:
107 target
= bpy
.data
.materials
.new(mu_new_material_name(material_name
))
108 target
.use_nodes
= True # When do we not want nodes today?
112 objects
= bpy
.context
.selected_editable_objects
115 # Apparently selected_editable_objects includes objects as cameras etc
116 if not obj
.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
119 # set the active object to our object
120 scene
= bpy
.context
.scene
121 bpy
.context
.view_layer
.objects
.active
= obj
123 if link_override
== 'KEEP':
124 if len(obj
.material_slots
) > 0:
125 link
= obj
.material_slots
[0].link
131 # If we should override all current material slots
132 if override_type
== 'OVERRIDE_ALL' or obj
.type == 'META':
134 # If there's more than one slot, Clear out all the material slots
135 if len(obj
.material_slots
) > 1:
136 mu_clear_materials(obj
)
138 # If there's no slots left/never was one, add a slot
139 if len(obj
.material_slots
) == 0:
140 bpy
.ops
.object.material_slot_add()
142 # Assign the material to that slot
143 obj
.material_slots
[0].link
= link
144 obj
.material_slots
[0].material
= target
146 if obj
.type == 'META':
147 self
.report({'INFO'}, "Meta balls only support one material, all other materials overridden!")
149 # If we should override each material slot
150 elif override_type
== 'OVERRIDE_SLOTS':
152 # go through each slot
153 for material
in obj
.material_slots
:
154 # assign the target material to current slot
155 if not link_override
== 'KEEP':
156 obj
.material_slots
[i
].link
= link
157 obj
.material_slots
[i
].material
= target
160 elif override_type
== 'OVERRIDE_CURRENT':
161 active_slot
= obj
.active_material_index
163 if len(obj
.material_slots
) == 0:
164 self
.report({'INFO'}, 'No material slots found! A material slot was added!')
165 bpy
.ops
.object.material_slot_add()
167 obj
.material_slots
[active_slot
].material
= target
169 # if we should keep the material slots and just append the selected material (if not already assigned)
170 elif override_type
== 'APPEND_MATERIAL':
173 material_slots
= obj
.material_slots
175 if (obj
.data
.users
> 1) and (len(material_slots
) >= 1 and material_slots
[0].link
== 'OBJECT'):
176 self
.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' +
177 'Unwanted results might happen!')
179 # check material slots for material_name materia
180 for material
in material_slots
:
181 if material
.name
== material_name
:
186 obj
.active_material_index
= i
191 # In Edit mode, or if there's not a slot, append the assigned material
192 # If we're overriding, there's currently no materials at all, so after this there will be 1
193 # If not, this adds another slot with the assigned material
195 index
= len(obj
.material_slots
)
196 bpy
.ops
.object.material_slot_add()
197 obj
.material_slots
[index
].link
= link
198 obj
.material_slots
[index
].material
= target
199 obj
.active_material_index
= index
201 mu_assign_to_data(obj
, target
, index
, edit_mode
, all_polygons
)
203 # We shouldn't risk unsetting the active object
204 if not active_object
is None:
205 # restore the active object
206 bpy
.context
.view_layer
.objects
.active
= active_object
209 bpy
.ops
.object.mode_set(mode
='EDIT')
214 def mu_select_by_material_name(self
, find_material_name
, extend_selection
= False, internal
= False):
215 """Searches through all objects, or the polygons/curves of the current object
216 to find and select objects/data with the desired material"""
218 # in object mode selects all objects with material find_material_name
219 # in edit mode selects all polygons with material find_material_name
221 find_material
= bpy
.data
.materials
.get(find_material_name
)
223 if find_material
is None:
224 self
.report({'INFO'}, "The material " + find_material_name
+ " doesn't exists!")
225 return {'CANCELLED'} if not internal
else -1
227 # check for edit_mode
229 found_material
= False
231 scene
= bpy
.context
.scene
233 # set selection mode to polygons
234 scene
.tool_settings
.mesh_select_mode
= False, False, True
236 active_object
= bpy
.context
.active_object
238 if (not active_object
is None) and (active_object
.mode
== 'EDIT'):
242 objects
= bpy
.context
.visible_objects
245 if obj
.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
246 mat_slots
= obj
.material_slots
247 for material
in mat_slots
:
248 if material
.material
== find_material
:
249 obj
.select_set(state
= True)
251 found_material
= True
253 # the active object may not have the material!
254 # set it to one that does!
255 bpy
.context
.view_layer
.objects
.active
= obj
258 if not extend_selection
:
259 obj
.select_set(state
=False)
262 elif not extend_selection
:
263 obj
.select_set(state
=False)
265 if not found_material
:
267 self
.report({'INFO'}, "No objects found with the material " +
268 find_material_name
+ "!")
269 return {'FINISHED'} if not internal
else 0
272 # it's edit_mode, so select the polygons
274 if active_object
.type == 'MESH':
275 # if not extending the selection, deselect all first
276 # (Without this, edges/faces were still selected
277 # while the faces were deselected)
278 if not extend_selection
:
279 bpy
.ops
.mesh
.select_all(action
= 'DESELECT')
281 objects
= bpy
.context
.selected_editable_objects
284 bpy
.context
.view_layer
.objects
.active
= obj
286 if obj
.type == 'MESH':
287 bpy
.ops
.object.mode_set()
289 mat_slots
= obj
.material_slots
291 # same material can be on multiple slots
294 for material
in mat_slots
:
295 if material
.material
== find_material
:
296 slot_indeces
.append(i
)
301 for poly
in mesh
.polygons
:
302 if poly
.material_index
in slot_indeces
:
304 found_material
= True
305 elif not extend_selection
:
310 bpy
.ops
.object.mode_set(mode
= 'EDIT')
313 elif obj
.type in {'CURVE', 'SURFACE'}:
314 # For Curve objects, there can only be one material per spline
315 # and thus each spline is linked to one material slot.
316 # So to not have to care for different data structures
317 # for different curve types, we use the material slots
318 # and the built in selection methods
319 # (Technically, this should work for meshes as well)
321 mat_slots
= obj
.material_slots
324 for material
in mat_slots
:
325 bpy
.context
.active_object
.active_material_index
= i
327 if material
.material
== find_material
:
328 bpy
.ops
.object.material_slot_select()
329 found_material
= True
330 elif not extend_selection
:
331 bpy
.ops
.object.material_slot_deselect()
336 # Some object types are not supported
337 # mostly because don't really support selecting by material (like Font/Text objects)
338 # or that they don't support multiple materials/are just "weird" (i.e. Meta balls)
339 self
.report({'WARNING'}, "The type '" +
341 "' isn't supported in Edit mode by Material Utilities!")
342 #return {'CANCELLED'}
344 bpy
.context
.view_layer
.objects
.active
= active_object
346 if (not found_material
) and (not internal
):
347 self
.report({'INFO'}, "Material " + find_material_name
+ " isn't assigned to anything!")
349 return {'FINISHED'} if not internal
else 1
352 def mu_copy_material_to_others(self
):
353 """Copy the material to of the current object to the other seleceted all_objects"""
354 # Currently uses the built-in method
355 # This could be extended to work in edit mode as well
357 #active_object = context.active_object
359 bpy
.ops
.object.material_slot_copy()
364 def mu_cleanmatslots(self
, affect
):
365 """Clean the material slots of the seleceted objects"""
367 # check for edit mode
369 active_object
= bpy
.context
.active_object
370 if active_object
.mode
== 'EDIT':
372 bpy
.ops
.object.mode_set()
376 if affect
== 'ACTIVE':
377 objects
= [active_object
]
378 elif affect
== 'SELECTED':
379 objects
= bpy
.context
.selected_editable_objects
380 elif affect
== 'SCENE':
381 objects
= bpy
.context
.scene
.objects
382 else: # affect == 'ALL'
383 objects
= bpy
.data
.objects
386 used_mat_index
= [] # we'll store used materials indices here
387 assigned_materials
= []
391 materials
= obj
.material_slots
.keys()
393 if obj
.type == 'MESH':
394 # check the polygons on the mesh to build a list of used materials
397 for poly
in mesh
.polygons
:
398 # get the material index for this face...
399 material_index
= poly
.material_index
401 if material_index
>= len(materials
):
403 self
.report({'ERROR'},
404 "A poly with an invalid material was found, this should not happen! Canceling!")
407 # indices will be lost: Store face mat use by name
408 current_mat
= materials
[material_index
]
409 assigned_materials
.append(current_mat
)
411 # check if index is already listed as used or not
413 for mat
in used_mat_index
:
414 if mat
== material_index
:
418 # add this index to the list
419 used_mat_index
.append(material_index
)
421 # re-assign the used materials to the mesh and leave out the unused
422 for u
in used_mat_index
:
423 material_list
.append(materials
[u
])
424 # we'll need a list of names to get the face indices...
425 material_names
.append(materials
[u
])
427 mu_assign_material_slots(obj
, material_list
)
429 # restore face indices:
431 for poly
in mesh
.polygons
:
432 material_index
= material_names
.index(assigned_materials
[i
])
433 poly
.material_index
= material_index
436 elif obj
.type in {'CURVE', 'SURFACE'}:
438 splines
= obj
.data
.splines
440 for spline
in splines
:
441 # Get the material index of this spline
442 material_index
= spline
.material_index
444 # indices will be last: Store material use by name
445 current_mat
= materials
[material_index
]
446 assigned_materials
.append(current_mat
)
448 # check if indek is already listed as used or not
450 for mat
in used_mat_index
:
451 if mat
== material_index
:
455 # add this index to the list
456 used_mat_index
.append(material_index
)
458 # re-assigned the used materials to the curve and leave out the unused
459 for u
in used_mat_index
:
460 material_list
.append(materials
[u
])
461 # we'll need a list of names to get the face indices
462 material_names
.append(materials
[u
])
464 mu_assign_material_slots(obj
, material_list
)
466 # restore spline indices
468 for spline
in splines
:
469 material_index
= material_names
.index(assigned_materials
[i
])
470 spline
.material_index
= material_index
474 # Some object types are not supported
475 self
.report({'WARNING'},
476 "The type '" + obj
.type + "' isn't currently supported " +
477 "for Material slots cleaning by Material Utilities!")
480 bpy
.ops
.object.mode_set(mode
='EDIT')
484 def mu_remove_material(self
, for_active_object
= False):
485 """Remove the active material slot from selected object(s)"""
487 if for_active_object
:
488 bpy
.ops
.object.material_slot_remove()
490 last_active
= bpy
.context
.active_object
491 objects
= bpy
.context
.selected_editable_objects
494 bpy
.context
.view_layer
.objects
.active
= obj
495 bpy
.ops
.object.material_slot_remove()
497 bpy
.context
.view_layer
.objects
.active
= last_active
501 def mu_remove_all_materials(self
, for_active_object
= False):
502 """Remove all material slots from selected object(s)"""
504 if for_active_object
:
505 obj
= bpy
.context
.active_object
507 # Clear out the material slots
508 obj
.data
.materials
.clear()
511 last_active
= bpy
.context
.active_object
512 objects
= bpy
.context
.selected_editable_objects
515 obj
.data
.materials
.clear()
517 bpy
.context
.view_layer
.objects
.active
= last_active
522 def mu_replace_material(material_a
, material_b
, all_objects
=False, update_selection
=False):
523 """Replace one material with another material"""
525 # material_a is the name of original material
526 # material_b is the name of the material to replace it with
527 # 'all' will replace throughout the blend file
529 mat_org
= bpy
.data
.materials
.get(material_a
)
530 mat_rep
= bpy
.data
.materials
.get(material_b
)
532 if mat_org
!= mat_rep
and None not in (mat_org
, mat_rep
):
533 # Store active object
534 scn
= bpy
.context
.scene
537 objs
= bpy
.data
.objects
539 objs
= bpy
.context
.selected_editable_objects
542 if obj
.type == 'MESH':
545 for mat
in obj
.material_slots
:
546 if mat
.material
== mat_org
:
547 mat
.material
= mat_rep
549 # Indicate which objects were affected
551 obj
.select_set(state
= True)
554 if update_selection
and not match
:
555 obj
.select_set(state
= False)
560 def mu_set_fake_user(self
, fake_user
, materials
):
561 """Set the fake user flag for the objects material"""
563 if materials
== 'ALL':
564 mats
= (mat
for mat
in bpy
.data
.materials
if mat
.library
is None)
565 elif materials
== 'UNUSED':
566 mats
= (mat
for mat
in bpy
.data
.materials
if mat
.library
is None and mat
.users
== 0)
569 if materials
== 'ACTIVE':
570 objs
= [bpy
.context
.active_object
]
571 elif materials
== 'SELECTED':
572 objs
= bpy
.context
.selected_objects
573 elif materials
== 'SCENE':
574 objs
= bpy
.context
.scene
.objects
575 else: # materials == 'USED'
576 objs
= bpy
.data
.objects
577 # Maybe check for users > 0 instead?
579 mats
= (mat
for ob
in objs
580 if hasattr(ob
.data
, "materials")
581 for mat
in ob
.data
.materials
582 if mat
.library
is None)
584 if fake_user
== 'TOGGLE':
587 if not mat
.name
in done_mats
:
588 mat
.use_fake_user
= not mat
.use_fake_user
589 done_mats
.append(mat
.name
)
591 fake_user_val
= fake_user
== 'ON'
593 mat
.use_fake_user
= fake_user_val
595 for area
in bpy
.context
.screen
.areas
:
596 if area
.type in ('PROPERTIES', 'NODE_EDITOR'):
602 def mu_change_material_link(self
, link
, affect
, override_data_material
= False):
603 """Change what the materials are linked to (Object or Data), while keeping materials assigned"""
607 if affect
== "ACTIVE":
608 objects
= [bpy
.context
.active_object
]
609 elif affect
== "SELECTED":
610 objects
= bpy
.context
.selected_objects
611 elif affect
== "SCENE":
612 objects
= bpy
.context
.scene
.objects
613 elif affect
== "ALL":
614 objects
= bpy
.data
.objects
616 for object in objects
:
618 for slot
in object.material_slots
:
619 present_material
= slot
.material
622 slot
.link
= ('DATA' if slot
.link
== 'OBJECT' else 'OBJECT')
626 if slot
.link
== 'OBJECT':
627 override_data_material
= True
628 elif slot
.material
is None:
629 override_data_material
= True
630 elif not override_data_material
:
631 self
.report({'INFO'},
632 'The object Data for object ' + object.name_full
+ ' already had a material assigned ' +
633 'to slot #' + str(index
) + ' (' + slot
.material
.name
+ '), it was not overridden!')
635 if override_data_material
:
636 slot
.material
= present_material
642 def mu_join_objects(self
, materials
):
643 """Join objects together based on their material"""
645 for material
in materials
:
646 mu_select_by_material_name(self
, material
, False, True)
648 bpy
.ops
.object.join()
652 def mu_set_auto_smooth(self
, angle
, affect
, set_smooth_shading
):
653 """Set Auto smooth values for selected objects"""
659 if affect
== "ACTIVE":
660 objects
= [bpy
.context
.active_object
]
661 elif affect
== "SELECTED":
662 objects
= bpy
.context
.selected_editable_objects
663 elif affect
== "SCENE":
664 objects
= bpy
.context
.scene
.objects
665 elif affect
== "ALL":
666 objects
= bpy
.data
.objects
668 if len(objects
) == 0:
669 self
.report({'WARNING'}, 'No objects available to set Auto Smooth on')
672 for object in objects
:
673 if object.type == "MESH":
674 if set_smooth_shading
:
675 for poly
in object.data
.polygons
:
676 poly
.use_smooth
= True
678 #bpy.ops.object.shade_smooth()
680 object.data
.set_sharp_from_angle(angle
=angle
) # 35 degrees as radians
682 objects_affected
+= 1
684 self
.report({'INFO'}, 'Auto smooth angle set to %.0f° on %d of %d objects' %
685 (degrees(angle
), objects_affected
, len(objects
)))