diff options
Diffstat (limited to 'materials_utils/functions.py')
-rw-r--r-- | materials_utils/functions.py | 604 |
1 files changed, 604 insertions, 0 deletions
diff --git a/materials_utils/functions.py b/materials_utils/functions.py new file mode 100644 index 00000000..6c8c72bd --- /dev/null +++ b/materials_utils/functions.py @@ -0,0 +1,604 @@ +import bpy + +# ----------------------------------------------------------------------------- +# utility functions + +def mu_assign_material_slots(object, material_list): + """Given an object and a list of material names removes all material slots from the object + adds new ones for each material in the material list, adds the materials to the slots as well.""" + + scene = bpy.context.scene + active_object = bpy.context.active_object + bpy.context.view_layer.objects.active = object + + for s in object.material_slots: + bpy.ops.object.material_slot_remove() + + # re-add them and assign material + i = 0 + for mat in material_list: + material = bpy.data.materials[mat] + object.data.materials.append(material) + i += 1 + + # restore active object: + bpy.context.view_layer.objects.active = active_object + +def mu_assign_to_data(object, material, index, edit_mode, all = True): + """Assign the material to the object data (polygons/splines)""" + + if object.type == 'MESH': + # now assign the material to the mesh + mesh = object.data + if all: + for poly in mesh.polygons: + poly.material_index = index + else: + for poly in mesh.polygons: + if poly.select: + poly.material_index = index + + mesh.update() + + elif object.type in {'CURVE', 'SURFACE', 'TEXT'}: + bpy.ops.object.mode_set(mode = 'EDIT') # This only works in Edit mode + + # If operator was run in Object mode + if not edit_mode: + # Select everything in Edit mode + bpy.ops.curve.select_all(action = 'SELECT') + + bpy.ops.object.material_slot_assign() # Assign material of the current slot to selection + + if not edit_mode: + bpy.ops.object.mode_set(mode = 'OBJECT') + +def mu_new_material_name(material): + return material + + +def mu_clear_materials(object): + #obj.data.materials.clear() + + for mat in object.material_slots: + bpy.ops.object.material_slot_remove() + + +def mu_assign_material(self, material_name = "Default", override_type = 'APPEND_MATERIAL', link_override = 'KEEP'): + """Assign the defined material to selected polygons/objects""" + + # get active object so we can restore it later + active_object = bpy.context.active_object + + edit_mode = False + all_polygons = True + if (not active_object is None) and active_object.mode == 'EDIT': + edit_mode = True + all_polygons = False + bpy.ops.object.mode_set() + + # check if material exists, if it doesn't then create it + found = False + for material in bpy.data.materials: + if material.name == material_name: + target = material + found = True + break + + if not found: + target = bpy.data.materials.new(mu_new_material_name(material_name)) + target.use_nodes = True # When do we not want nodes today? + + + index = 0 + objects = bpy.context.selected_editable_objects + + for obj in objects: + # Apparently selected_editable_objects includes objects as cameras etc + if not obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}: + continue + + # set the active object to our object + scene = bpy.context.scene + bpy.context.view_layer.objects.active = obj + + if link_override == 'KEEP': + if len(obj.material_slots) > 0: + link = obj.material_slots[0].link + else: + link = 'DATA' + else: + link = link_override + + # If we should override all current material slots + if override_type == 'OVERRIDE_ALL' or obj.type == 'META': + + # If there's more than one slot, Clear out all the material slots + if len(obj.material_slots) > 1: + mu_clear_materials(obj) + + # If there's no slots left/never was one, add a slot + if len(obj.material_slots) == 0: + bpy.ops.object.material_slot_add() + + # Assign the material to that slot + obj.material_slots[0].link = link + obj.material_slots[0].material = target + + if obj.type == 'META': + self.report({'INFO'}, "Meta balls only support one material, all other materials overriden!") + + # If we should override each material slot + elif override_type == 'OVERRIDE_SLOTS': + i = 0 + # go through each slot + for material in obj.material_slots: + # assign the target material to current slot + if not link_override == 'KEEP': + obj.material_slots[i].link = link + obj.material_slots[i].material = target + i += 1 + + # if we should keep the material slots and just append the selected material (if not already assigned) + elif override_type == 'APPEND_MATERIAL': + found = False + i = 0 + material_slots = obj.material_slots + + if (obj.data.users > 1) and (len(material_slots) >= 1 and material_slots[0].link == 'OBJECT'): + self.report({'WARNING'}, 'Append material is not recommended for linked duplicates! ' + + 'Unwanted results might happen!') + + # check material slots for material_name materia + for material in material_slots: + if material.name == material_name: + found = True + index = i + + # make slot active + obj.active_material_index = i + break + i += 1 + + if not found: + # In Edit mode, or if there's not a slot, append the assigned material + # If we're overriding, there's currently no materials at all, so after this there will be 1 + # If not, this adds another slot with the assigned material + + index = len(obj.material_slots) + bpy.ops.object.material_slot_add() + obj.material_slots[index].link = link + obj.material_slots[index].material = target + obj.active_material_index = index + + mu_assign_to_data(obj, target, index, edit_mode, all_polygons) + + # We shouldn't risk unsetting the active object + if not active_object is None: + # restore the active object + bpy.context.view_layer.objects.active = active_object + + if edit_mode: + bpy.ops.object.mode_set(mode='EDIT') + + return {'FINISHED'} + + +def mu_select_by_material_name(self, find_material_name, extend_selection = False): + """Searches through all objects, or the polygons/curves of the current object + to find and select objects/data with the desired material""" + + # in object mode selects all objects with material find_material_name + # in edit mode selects all polygons with material find_material_name + + find_material = bpy.data.materials.get(find_material_name) + + if find_material is None: + self.report({'INFO'}, "The material " + find_material_name + " doesn't exists!") + return {'CANCELLED'} + + # check for edit_mode + edit_mode = False + found_material = False + + scene = bpy.context.scene + + # set selection mode to polygons + scene.tool_settings.mesh_select_mode = False, False, True + + active_object = bpy.context.active_object + + if (not active_object is None) and (active_object.mode == 'EDIT'): + edit_mode = True + + if not edit_mode: + objects = bpy.context.visible_objects + + for obj in objects: + if obj.type in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}: + mat_slots = obj.material_slots + for material in mat_slots: + if material.material == find_material: + obj.select_set(state = True) + + found_material = True + + # the active object may not have the material! + # set it to one that does! + bpy.context.view_layer.objects.active = obj + break + else: + if not extend_selection: + obj.select_set(state=False) + + #deselect non-meshes + elif not extend_selection: + obj.select_set(state=False) + + if not found_material: + self.report({'INFO'}, "No objects found with the material " + + find_material_name + "!") + return {'FINISHED'} + + else: + # it's edit_mode, so select the polygons + + if active_object.type == 'MESH': + # if not extending the selection, deselect all first + # (Without this, edges/faces were still selected + # while the faces were deselcted) + if not extend_selection: + bpy.ops.mesh.select_all(action = 'DESELECT') + + objects = bpy.context.selected_editable_objects + + for obj in objects: + print("Obj:" + obj.name) + bpy.context.view_layer.objects.active = obj + + if obj.type == 'MESH': + bpy.ops.object.mode_set() + + mat_slots = obj.material_slots + + # same material can be on multiple slots + slot_indeces = [] + i = 0 + for material in mat_slots: + if material.material == find_material: + slot_indeces.append(i) + i += 1 + + mesh = obj.data + + for poly in mesh.polygons: + if poly.material_index in slot_indeces: + poly.select = True + found_material = True + elif not extend_selection: + poly.select = False + + mesh.update() + + bpy.ops.object.mode_set(mode = 'EDIT') + + + elif obj.type in {'CURVE', 'SURFACE'}: + # For Curve objects, there can only be one material per spline + # and thus each spline is linked to one material slot. + # So to not have to care for different data structures + # for different curve types, we use the material slots + # and the built in selection methods + # (Technically, this should work for meshes as well) + + mat_slots = obj.material_slots + + i = 0 + for material in mat_slots: + bpy.context.active_object.active_material_index = i + + if material.material == find_material: + bpy.ops.object.material_slot_select() + found_material = True + elif not extend_selection: + bpy.ops.object.material_slot_deselect() + + i += 1 + + else: + # Some object types are not supported + # mostly because don't really support selecting by material (like Font/Text objects) + # ore that they don't support multiple materials/are just "weird" (i.e. Meta balls) + self.report({'WARNING'}, "The type '" + + obj.type + + "' isn't supported in Edit mode by Material Utilities!") + #return {'CANCELLED'} + + bpy.context.view_layer.objects.active = active_object + + if not found_material: + self.report({'INFO'}, "Material " + find_material_name + " isn't assigned to anything!") + + return {'FINISHED'} + + +def mu_copy_material_to_others(self): + """Copy the material to of the current object to the other seleceted all_objects""" + # Currently uses the built-in method + # This could be extended to work in edit mode as well + + #active_object = context.active_object + + bpy.ops.object.material_slot_copy() + + return {'FINISHED'} + + +def mu_cleanmatslots(self): + """Clean the material slots of the seleceted objects""" + + # check for edit mode + edit_mode = False + active_object = bpy.context.active_object + if active_object.mode == 'EDIT': + edit_mode = True + bpy.ops.object.mode_set() + + objects = bpy.context.selected_editable_objects + + for obj in objects: + used_mat_index = [] # we'll store used materials indices here + assigned_materials = [] + material_list = [] + material_names = [] + + materials = obj.material_slots.keys() + + if obj.type == 'MESH': + # check the polygons on the mesh to build a list of used materials + mesh = obj.data + + for poly in mesh.polygons: + # get the material index for this face... + material_index = poly.material_index + + if material_index >= len(materials): + poly.select = True + self.report({'ERROR'}, + "A poly with an invalid material was found, this should not happen! Canceling!") + return {'CANCELLED'} + + # indices will be lost: Store face mat use by name + current_mat = materials[material_index] + assigned_materials.append(current_mat) + + # check if index is already listed as used or not + found = False + for mat in used_mat_index: + if mat == material_index: + found = True + + if not found: + # add this index to the list + used_mat_index.append(material_index) + + # re-assign the used materials to the mesh and leave out the unused + for u in used_mat_index: + material_list.append(materials[u]) + # we'll need a list of names to get the face indices... + material_names.append(materials[u]) + + mu_assign_material_slots(obj, material_list) + + # restore face indices: + i = 0 + for poly in mesh.polygons: + material_index = material_names.index(assigned_materials[i]) + poly.material_index = material_index + i += 1 + + elif obj.type in {'CURVE', 'SURFACE'}: + + splines = obj.data.splines + + for spline in splines: + # Get the material index of this spline + material_index = spline.material_index + + # indices will be last: Store material use by name + current_mat = materials[material_index] + assigned_materials.append(current_mat) + + # check if indek is already listed as used or not + found = False + for mat in used_mat_index: + if mat == material_index: + found = True + + if not found: + # add this index to the list + used_mat_index.append(material_index) + + # re-assigned the used materials to the curve and leave out the unused + for u in used_mat_index: + material_list.append(materials[u]) + # we'll need a list of names to get the face indices + material_names.append(materials[u]) + + mu_assign_material_slots(obj, material_list) + + # restore spline indices + i = 0 + for spline in splines: + material_index = material_names.index(assigned_materials[i]) + spline.material_index = material_index + i += 1 + + else: + # Some object types are not supported + self.report({'WARNING'}, + "The type '" + obj.type + "' isn't currently supported " + + "for Material slots cleaning by Material Utilities!") + + if edit_mode: + bpy.ops.object.mode_set(mode='EDIT') + + return {'FINISHED'} + +def mu_remove_material(self, for_active_object = False): + """Remove the active material slot from selected object(s)""" + + if for_active_object: + bpy.ops.object.material_slot_remove() + else: + last_active = bpy.context.active_object + objects = bpy.context.selected_editable_objects + + for obj in objects: + bpy.context.view_layer.objects.active = obj + bpy.ops.object.material_slot_remove() + + bpy.context.view_layer.objects.active = last_active + + return {'FINISHED'} + +def mu_remove_all_materials(self, for_active_object = False): + """Remove all material slots from selected object(s)""" + + if for_active_object: + obj = bpy.context.active_object + + # Clear out the material slots + obj.data.materials.clear() + + else: + last_active = bpy.context.active_object + objects = bpy.context.selected_editable_objects + + for obj in objects: + obj.data.materials.clear() + + bpy.context.view_layer.objects.active = last_active + + return {'FINISHED'} + + +def mu_replace_material(material_a, material_b, all_objects=False, update_selection=False): + """Replace one material with another material""" + + # material_a is the name of original material + # material_b is the name of the material to replace it with + # 'all' will replace throughout the blend file + + mat_org = bpy.data.materials.get(material_a) + mat_rep = bpy.data.materials.get(material_b) + + if mat_org != mat_rep and None not in (mat_org, mat_rep): + # Store active object + scn = bpy.context.scene + + if all_objects: + objs = bpy.data.objects + else: + objs = bpy.context.selected_editable_objects + + for obj in objs: + if obj.type == 'MESH': + match = False + + for mat in obj.material_slots: + if mat.material == mat_org: + mat.material = mat_rep + + # Indicate which objects were affected + if update_selection: + obj.select_set(state = True) + match = True + + if update_selection and not match: + obj.select_set(state = False) + + return {'FINISHED'} + + +def mu_set_fake_user(self, fake_user, materials): + """Set the fake user flag for the objects material""" + + if materials == 'ALL': + mats = (mat for mat in bpy.data.materials if mat.library is None) + elif materials == 'UNUSED': + mats = (mat for mat in bpy.data.materials if mat.library is None and mat.users == 0) + else: + mats = [] + if materials == 'ACTIVE': + objs = [bpy.context.active_object] + elif materials == 'SELECTED': + objs = bpy.context.selected_objects + elif materials == 'SCENE': + objs = bpy.context.scene.objects + else: # materials == 'USED' + objs = bpy.data.objects + # Maybe check for users > 0 instead? + + mats = (mat for ob in objs + if hasattr(ob.data, "materials") + for mat in ob.data.materials + if mat.library is None) + + if fake_user == 'TOGGLE': + done_mats = [] + for mat in mats: + if not mat.name in done_mats: + mat.use_fake_user = not mat.use_fake_user + done_mats.append(mat.name) + else: + fake_user_val = fake_user == 'ON' + for mat in mats: + mat.use_fake_user = fake_user_val + + for area in bpy.context.screen.areas: + if area.type in ('PROPERTIES', 'NODE_EDITOR'): + area.tag_redraw() + + return {'FINISHED'} + + +def mu_change_material_link(self, link, affect, override_data_material = False): + """Change what the materials are linked to (Object or Data), while keeping materials assigned""" + + objects = [] + + if affect == "ACTIVE": + objects = [bpy.context.active_object] + elif affect == "SELECTED": + objects = bpy.context.selected_objects + elif affect == "SCENE": + objects = bpy.context.scene.objects + elif affect == "ALL": + objects = bpy.data.objects + + for object in objects: + index = 0 + for slot in object.material_slots: + present_material = slot.material + + if link == 'TOGGLE': + slot.link = ('DATA' if slot.link == 'OBJECT' else 'OBJECT') + else: + slot.link = link + + if slot.link == 'OBJECT': + override_data_material = True + elif slot.material is None: + override_data_material = True + elif not override_data_material: + self.report({'INFO'}, + 'The object Data for object ' + object.name_full + ' already had a material assigned ' + + 'to slot #' + str(index) + ' (' + slot.material.name + '), it was not overriden!') + + if override_data_material: + slot.material = present_material + + index = index + 1 + + return {'FINISHED'} |