1 # SPDX-License-Identifier: GPL-2.0-or-later
4 from math
import radians
, degrees
6 # -----------------------------------------------------------------------------
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
22 for mat
in material_list
:
23 material
= bpy
.data
.materials
[mat
]
24 object.data
.materials
.append(material
)
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
37 for poly
in mesh
.polygons
:
38 poly
.material_index
= index
40 for poly
in mesh
.polygons
:
42 poly
.material_index
= index
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
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
57 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
59 def mu_new_material_name(material
):
60 for mat
in bpy
.data
.materials
:
63 if (name
== material
):
65 base
, suffix
= name
.rsplit('.', 1)
67 # trigger the exception
69 material
= base
+ "." + '%03d' % (num
+ 1)
71 material
= material
+ ".001"
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
91 if (not active_object
is None) and active_object
.mode
== 'EDIT':
94 bpy
.ops
.object.mode_set()
96 # check if material exists, if it doesn't then create it
98 for material
in bpy
.data
.materials
:
99 if material
.name
== material_name
:
105 target
= bpy
.data
.materials
.new(mu_new_material_name(material_name
))
106 target
.use_nodes
= True # When do we not want nodes today?
110 objects
= bpy
.context
.selected_editable_objects
113 # Apparently selected_editable_objects includes objects as cameras etc
114 if not obj
.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
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
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':
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
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':
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
:
184 obj
.active_material_index
= i
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
207 bpy
.ops
.object.mode_set(mode
='EDIT')
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
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'):
240 objects
= bpy
.context
.visible_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
256 if not extend_selection
:
257 obj
.select_set(state
=False)
260 elif not extend_selection
:
261 obj
.select_set(state
=False)
263 if not found_material
:
265 self
.report({'INFO'}, "No objects found with the material " +
266 find_material_name
+ "!")
267 return {'FINISHED'} if not internal
else 0
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
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
292 for material
in mat_slots
:
293 if material
.material
== find_material
:
294 slot_indeces
.append(i
)
299 for poly
in mesh
.polygons
:
300 if poly
.material_index
in slot_indeces
:
302 found_material
= True
303 elif not extend_selection
:
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
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()
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 '" +
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()
362 def mu_cleanmatslots(self
, affect
):
363 """Clean the material slots of the seleceted objects"""
365 # check for edit mode
367 active_object
= bpy
.context
.active_object
368 if active_object
.mode
== 'EDIT':
370 bpy
.ops
.object.mode_set()
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
384 used_mat_index
= [] # we'll store used materials indices here
385 assigned_materials
= []
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
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
):
401 self
.report({'ERROR'},
402 "A poly with an invalid material was found, this should not happen! Canceling!")
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
411 for mat
in used_mat_index
:
412 if mat
== material_index
:
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:
429 for poly
in mesh
.polygons
:
430 material_index
= material_names
.index(assigned_materials
[i
])
431 poly
.material_index
= material_index
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
448 for mat
in used_mat_index
:
449 if mat
== material_index
:
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
466 for spline
in splines
:
467 material_index
= material_names
.index(assigned_materials
[i
])
468 spline
.material_index
= material_index
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!")
478 bpy
.ops
.object.mode_set(mode
='EDIT')
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()
488 last_active
= bpy
.context
.active_object
489 objects
= bpy
.context
.selected_editable_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
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()
509 last_active
= bpy
.context
.active_object
510 objects
= bpy
.context
.selected_editable_objects
513 obj
.data
.materials
.clear()
515 bpy
.context
.view_layer
.objects
.active
= last_active
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
535 objs
= bpy
.data
.objects
537 objs
= bpy
.context
.selected_editable_objects
540 if obj
.type == 'MESH':
543 for mat
in obj
.material_slots
:
544 if mat
.material
== mat_org
:
545 mat
.material
= mat_rep
547 # Indicate which objects were affected
549 obj
.select_set(state
= True)
552 if update_selection
and not match
:
553 obj
.select_set(state
= False)
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)
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':
585 if not mat
.name
in done_mats
:
586 mat
.use_fake_user
= not mat
.use_fake_user
587 done_mats
.append(mat
.name
)
589 fake_user_val
= fake_user
== 'ON'
591 mat
.use_fake_user
= fake_user_val
593 for area
in bpy
.context
.screen
.areas
:
594 if area
.type in ('PROPERTIES', 'NODE_EDITOR'):
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"""
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
:
616 for slot
in object.material_slots
:
617 present_material
= slot
.material
620 slot
.link
= ('DATA' if slot
.link
== 'OBJECT' else 'OBJECT')
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
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()
650 def mu_set_auto_smooth(self
, angle
, affect
, set_smooth_shading
):
651 """Set Auto smooth values for selected objects"""
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')
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
)))