diff options
author | meta-androcto <meta.androcto1@gmail.com> | 2019-08-11 08:54:11 +0300 |
---|---|---|
committer | meta-androcto <meta.androcto1@gmail.com> | 2019-08-11 08:54:11 +0300 |
commit | 802904cf43da3deed2094520e17a79d8020d372a (patch) | |
tree | e2d3f20be12d63cd1b7ab7457a04f09979111b60 | |
parent | 1bba102d652cea832ae7da0d1d476739d2e1d8da (diff) |
materials_utils: return to release: T67990 T63750 01d80b8f602f
-rw-r--r-- | materials_utils/__init__.py | 179 | ||||
-rw-r--r-- | materials_utils/enum_values.py | 32 | ||||
-rw-r--r-- | materials_utils/functions.py | 604 | ||||
-rw-r--r-- | materials_utils/menus.py | 216 | ||||
-rw-r--r-- | materials_utils/operators.py | 525 |
5 files changed, 1556 insertions, 0 deletions
diff --git a/materials_utils/__init__.py b/materials_utils/__init__.py new file mode 100644 index 00000000..40f976d7 --- /dev/null +++ b/materials_utils/__init__.py @@ -0,0 +1,179 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# Based on 2010 version by MichaelW +# (c) 2016 meta-androcto, parts based on work by Saidenka, lijenstina +# Materials Utils: by MichaleW, meta-androcto, lijenstina, +# (some code thanks to: CoDEmanX, SynaGl0w, ideasman42) +# Link to base names: Sybren, Texture renamer: Yadoob +# Ported from 2.6/2.7 to 2.8x by Christopher Hindefjord (chrishinde) 2019 + +bl_info = { + "name": "Material Utils", + "author": "MichaleW, ChrisHinde", + "version": (1, 0, 6), + "blender": (2, 80, 0), + "location": "View3D > Shift + Q key", + "description": "Menu of material tools (assign, select..) in the 3D View", + "warning": "", + "wiki_url": "https://github.com/ChrisHinde/MaterialUtilities", + "category": "Material" +} + +""" +This script has several functions and operators, grouped for convenience: + +* assign material: + offers the user a list of ALL the materials in the blend file and an + additional "new" entry the chosen material will be assigned to all the + selected objects in object mode. + + in edit mode the selected polygons get the selected material applied. + + if the user chose "new" the new material can be renamed using the + "last operator" section of the toolbox. + + +* select by material + in object mode this offers the user a menu of all materials in the blend + file any objects using the selected material will become selected, any + objects without the material will be removed from selection. + + in edit mode: the menu offers only the materials attached to the current + object. It will select the polygons that use the material and deselect those + that do not. + +* clean material slots + for all selected objects any empty material slots or material slots with + materials that are not used by the mesh polygons or splines will be removed. + +* remove material slots + removes all material slots of the active (or selected) object(s). + +* replace materials + lets your replace one material by another. Optionally for all objects in + the blend, otherwise for selected editable objects only. An additional + option allows you to update object selection, to indicate which objects + were affected and which not. + +* set fake user + enable/disable fake user for materials. You can chose for which materials + it shall be set, materials of active / selected / objects in current scene + or used / unused / all materials. + +""" + + +import bpy + +from .enum_values import * +from .functions import * +from .operators import * +from .menus import * + +# All classes used by Material Utilities, that need to be registred +classes = ( + VIEW3D_OT_materialutilities_assign_material_object, + VIEW3D_OT_materialutilities_assign_material_edit, + VIEW3D_OT_materialutilities_select_by_material_name, + VIEW3D_OT_materialutilities_copy_material_to_others, + + VIEW3D_OT_materialutilities_clean_material_slots, + VIEW3D_OT_materialutilities_remove_material_slot, + VIEW3D_OT_materialutilities_remove_all_material_slots, + + VIEW3D_OT_materialutilities_replace_material, + VIEW3D_OT_materialutilities_fake_user_set, + VIEW3D_OT_materialutilities_change_material_link, + + MATERIAL_OT_materialutilities_merge_base_names, + + MATERIAL_OT_materialutilities_material_slot_move, + + VIEW3D_MT_materialutilities_assign_material, + VIEW3D_MT_materialutilities_select_by_material, + + VIEW3D_MT_materialutilities_clean_slots, + VIEW3D_MT_materialutilities_specials, + + VIEW3D_MT_materialutilities_main, +) + + +# This allows you to right click on a button and link to the manual +def materialutilities_manual_map(): + print("ManMap") + url_manual_prefix = "https://github.com/ChrisHinde/MaterialUtilities" + url_manual_map = [] + #url_manual_mapping = () + #("bpy.ops.view3d.materialutilities_*", ""), + #("bpy.ops.view3d.materialutilities_assign_material_edit", ""), + #("bpy.ops.view3d.materialutilities_select_by_material_name", ""),) + + for cls in classes: + if issubclass(cls, bpy.types.Operator): + url_manual_map.append(("bpy.ops." + cls.bl_idname, "")) + + url_manual_mapping = tuple(url_manual_map) + #print(url_manual_mapping) + return url_manual_prefix, url_manual_mapping + +mu_classes_register, mu_classes_unregister = bpy.utils.register_classes_factory(classes) + + +def register(): + """Register the classes of Material Utilities together with the default shortcut (Shift+Q)""" + mu_classes_register() + + bpy.types.VIEW3D_MT_object_context_menu.append(materialutilities_specials_menu) + + bpy.types.MATERIAL_MT_context_menu.prepend(materialutilities_menu_move) + bpy.types.MATERIAL_MT_context_menu.append(materialutilities_menu_functions) + + kc = bpy.context.window_manager.keyconfigs.addon + if kc: + km = kc.keymaps.new(name = "3D View", space_type = "VIEW_3D") + kmi = km.keymap_items.new('wm.call_menu', 'Q', 'PRESS', ctrl = False, shift = True) + kmi.properties.name = VIEW3D_MT_materialutilities_main.bl_idname + + bpy.utils.register_manual_map(materialutilities_manual_map) + + +def unregister(): + """Unregister the classes of Material Utilities together with the default shortcut for the menu""" + mu_classes_unregister() + + bpy.utils.unregister_manual_map(materialutilities_manual_map) + + bpy.types.VIEW3D_MT_object_context_menu.remove(materialutilities_specials_menu) + + bpy.types.MATERIAL_MT_context_menu.remove(materialutilities_menu_move) + bpy.types.MATERIAL_MT_context_menu.remove(materialutilities_menu_functions) + + kc = bpy.context.window_manager.keyconfigs.addon + if kc: + km = kc.keymaps["3D View"] + for kmi in km.keymap_items: + if kmi.idname == 'wm.call_menu': + if kmi.properties.name == VIEW3D_MT_materialutilities_main.bl_idname: + km.keymap_items.remove(kmi) + break + + +if __name__ == "__main__": + register() diff --git a/materials_utils/enum_values.py b/materials_utils/enum_values.py new file mode 100644 index 00000000..f7f8f65b --- /dev/null +++ b/materials_utils/enum_values.py @@ -0,0 +1,32 @@ +import bpy + + +mu_override_type_enums = [ + ('OVERRIDE_ALL', "Override all assigned slots", + "Remove any current material slots, and assign the current material"), + ('OVERRIDE_SLOTS', 'Assign material to each slot', + 'Keep the material slots, but assign the selected material in each slot'), + ('APPEND_MATERIAL', 'Append Material', + 'Add the material in a new slot, and assign it to the whole object') +] + +mu_fake_user_set_enums = (('ON', "On", "Enable fake user"), + ('OFF', "Off", "Disable fake user"), + ('TOGGLE', "Toggle", "Toggle fake user")) +mu_fake_user_materials_enums = (('ACTIVE', "Active object", "Materials of active object only"), + ('SELECTED', "Selected objects", "Materials of selected objects"), + ('SCENE', "Scene objects", "Materials of objects in current scene"), + ('USED', "Used", "All materials used by objects"), + ('UNUSED', "Unused", "Currently unused materials"), + ('ALL', "All", "All materials in this blend file")) + +mu_link_to_enums = (('DATA', "Data", "Link the materials to the data"), + ('OBJECT', "Object", "Link the materials to the object"), + ('TOGGLE', "Toggle", "Toggle what the materials are currently linked to")) +mu_link_affect_enums = (('ACTIVE', "Active object", "Materials of active object only"), + ('SELECTED', "Selected objects", "Materials of selected objects"), + ('SCENE', "Scene objects", "Materials of objects in current scene"), + ('ALL', "All", "All materials in this blend file")) + +mu_material_slot_move_enums = (('TOP', "Top", "Move slot to the top"), + ('BOTTOM', "Bottom", "Move slot to the bottom")) 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'} diff --git a/materials_utils/menus.py b/materials_utils/menus.py new file mode 100644 index 00000000..faf6da0c --- /dev/null +++ b/materials_utils/menus.py @@ -0,0 +1,216 @@ +import bpy + +from .functions import * +from .operators import * + +# ----------------------------------------------------------------------------- +# menu classes + +class VIEW3D_MT_materialutilities_assign_material(bpy.types.Menu): + """Menu for choosing which material should be assigned to current selection""" + # The menu is filled programmatically with available materials + + bl_idname = "VIEW3D_MT_materialutilities_assign_material" + bl_label = "Assign Material" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + bl_id = VIEW3D_OT_materialutilities_assign_material_object.bl_idname + obj = context.object + + if (not obj is None) and obj.mode == 'EDIT': + bl_id = VIEW3D_OT_materialutilities_assign_material_edit.bl_idname + + for material_name, material in bpy.data.materials.items(): + layout.operator(bl_id, + text = material_name, + icon_value = material.preview.icon_id).material_name = material_name + + layout.operator(bl_id, + text = "Add New Material", + icon = 'ADD').material_name = "Unnamed material" + + +class VIEW3D_MT_materialutilities_clean_slots(bpy.types.Menu): + """Menu for cleaning up the material slots""" + + bl_idname = "VIEW3D_MT_materialutilities_clean_slots" + bl_label = "Clean Slots" + + def draw(self, context): + layout = self.layout + + layout.label + layout.operator(VIEW3D_OT_materialutilities_clean_material_slots.bl_idname, + text = "Clean Material Slots", + icon = 'X') + layout.separator() + layout.operator(VIEW3D_OT_materialutilities_remove_material_slot.bl_idname, + text = "Remove Active Material Slot", + icon = 'REMOVE') + layout.operator(VIEW3D_OT_materialutilities_remove_all_material_slots.bl_idname, + text = "Remove All Material Slots", + icon = 'CANCEL') + + +class VIEW3D_MT_materialutilities_select_by_material(bpy.types.Menu): + """Menu for choosing which material should be used for selection""" + # The menu is filled programmatically with available materials + + bl_idname = "VIEW3D_MT_materialutilities_select_by_material" + bl_label = "Select by Material" + + def draw(self, context): + layout = self.layout + + obj = context.object + layout.label + + if obj is None or obj.mode == 'OBJECT': + #show all used materials in entire blend file + for material_name, material in bpy.data.materials.items(): + # There's no point in showing materials with 0 users + # (It will still show materials with fake user though) + if material.users > 0: + layout.operator(VIEW3D_OT_materialutilities_select_by_material_name.bl_idname, + text = material_name, + icon_value = material.preview.icon_id + ).material_name = material_name + + elif obj.mode == 'EDIT': + objects = context.selected_editable_objects + materials_added = [] + + for obj in objects: + #show only the materials on this object + material_slots = obj.material_slots + for material_slot in material_slots: + material = material_slot.material + + # Don't add a material that's already in the menu + if material.name in materials_added: + continue + + layout.operator(VIEW3D_OT_materialutilities_select_by_material_name.bl_idname, + text = material.name, + icon_value = material.preview.icon_id + ).material_name = material.name + + materials_added.append(material.name) + +class VIEW3D_MT_materialutilities_specials(bpy.types.Menu): + """Spcials menu for Material Utilities""" + + bl_idname = "VIEW3D_MT_materialutilities_specials" + bl_label = "Specials" + + def draw(self, context): + layout = self.layout + + #layout.operator(VIEW3D_OT_materialutilities_set_new_material_name.bl_idname, icon = "SETTINGS") + + #layout.separator() + + layout.operator(MATERIAL_OT_materialutilities_merge_base_names.bl_idname, + text = "Merge Base Names", + icon = "GREASEPENCIL") + + +class VIEW3D_MT_materialutilities_main(bpy.types.Menu): + """Main menu for Material Utilities""" + + bl_idname = "VIEW3D_MT_materialutilities_main" + bl_label = "Material Utilities" + + def draw(self, context): + obj = context.object + + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.menu(VIEW3D_MT_materialutilities_assign_material.bl_idname, + icon = 'ADD') + layout.menu(VIEW3D_MT_materialutilities_select_by_material.bl_idname, + icon = 'VIEWZOOM') + layout.separator() + + layout.operator(VIEW3D_OT_materialutilities_copy_material_to_others.bl_idname, + text = 'Copy Materials to Selected', + icon = 'COPY_ID') + + layout.separator() + + layout.menu(VIEW3D_MT_materialutilities_clean_slots.bl_idname, + icon = 'NODE_MATERIAL') + + layout.separator() + layout.operator(VIEW3D_OT_materialutilities_replace_material.bl_idname, + text = 'Replace Material', + icon = 'OVERLAY') + + layout.operator(VIEW3D_OT_materialutilities_fake_user_set.bl_idname, + text = 'Set Fake User', + icon = 'FAKE_USER_OFF') + + layout.operator(VIEW3D_OT_materialutilities_change_material_link.bl_idname, + text = 'Change Material Link', + icon = 'LINKED') + layout.separator() + + layout.menu(VIEW3D_MT_materialutilities_specials.bl_idname, + icon = 'SOLO_ON') + + + +def materialutilities_specials_menu(self, contxt): + self.layout.separator() + self.layout.menu(VIEW3D_MT_materialutilities_main.bl_idname) + + +def materialutilities_menu_move(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator(MATERIAL_OT_materialutilities_material_slot_move.bl_idname, + icon = 'TRIA_UP_BAR', + text = 'Move Slot to the Top').movement = 'TOP' + layout.operator(MATERIAL_OT_materialutilities_material_slot_move.bl_idname, + icon = 'TRIA_DOWN_BAR', + text = 'Move Slot to the Bottom').movement = 'BOTTOM' + layout.separator() + +def materialutilities_menu_functions(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.separator() + + layout.menu(VIEW3D_MT_materialutilities_assign_material.bl_idname, + icon = 'ADD') + layout.menu(VIEW3D_MT_materialutilities_select_by_material.bl_idname, + icon = 'VIEWZOOM') + layout.separator() + + layout.separator() + + layout.menu(VIEW3D_MT_materialutilities_clean_slots.bl_idname, + icon = 'NODE_MATERIAL') + + layout.separator() + layout.operator(VIEW3D_OT_materialutilities_replace_material.bl_idname, + text = 'Replace Material', + icon = 'OVERLAY') + + layout.operator(VIEW3D_OT_materialutilities_fake_user_set.bl_idname, + text = 'Set Fake User', + icon = 'FAKE_USER_OFF') + + layout.operator(VIEW3D_OT_materialutilities_change_material_link.bl_idname, + text = 'Change Material Link', + icon = 'LINKED') + layout.separator() + + layout.menu(VIEW3D_MT_materialutilities_specials.bl_idname, + icon = 'SOLO_ON') diff --git a/materials_utils/operators.py b/materials_utils/operators.py new file mode 100644 index 00000000..6bb451e6 --- /dev/null +++ b/materials_utils/operators.py @@ -0,0 +1,525 @@ +import bpy + +from bpy.types import Operator +from bpy.props import StringProperty, BoolProperty, EnumProperty + + +from .enum_values import * +from .functions import * + +# ----------------------------------------------------------------------------- +# operator classes + +class VIEW3D_OT_materialutilities_assign_material_edit(bpy.types.Operator): + """Assign a material to the current selection""" + + bl_idname = "view3d.materialutilities_assign_material_edit" + bl_label = "Assign Material (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + material_name: StringProperty( + name = 'Material Name', + description = 'Name of Material to assign to current selection', + default = "", + maxlen = 63 + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + material_name = self.material_name + return mu_assign_material(self, material_name, 'APPEND_MATERIAL') + + + +class VIEW3D_OT_materialutilities_assign_material_object(bpy.types.Operator): + """Assign a material to the current selection + (See the operator panel [F9] for more options)""" + + bl_idname = "view3d.materialutilities_assign_material_object" + bl_label = "Assign Material (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + material_name: StringProperty( + name = 'Material Name', + description = 'Name of Material to assign to current selection', + default = "Unnamed Material", + maxlen = 63 + ) + override_type: EnumProperty( + name = 'Assignment method', + description = '', + items = mu_override_type_enums + ) + + @classmethod + def poll(cls, context): + return len(context.selected_editable_objects) > 0 + + def draw(self, context): + layout = self.layout + layout.prop_search(self, "material_name", bpy.data, "materials") + + layout.prop(self, "override_type") + + def execute(self, context): + material_name = self.material_name + override_type = self.override_type + result = mu_assign_material(self, material_name, override_type) + return result + + +class VIEW3D_OT_materialutilities_select_by_material_name(bpy.types.Operator): + """Select geometry that has the chosen material assigned to it + (See the operator panel [F9] for more options)""" + + bl_idname = "view3d.materialutilities_select_by_material_name" + bl_label = "Select By Material Name (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + extend_selection: BoolProperty( + name = 'Extend Selection', + description = 'Keeps the current selection and adds faces with the material to the selection' + ) + material_name: StringProperty( + name = 'Material Name', + description = 'Name of Material to find and Select', + maxlen = 63 + ) + + @classmethod + def poll(cls, context): + return len(context.visible_objects) > 0 + + def draw(self, context): + layout = self.layout + layout.prop_search(self, "material_name", bpy.data, "materials") + + layout.prop(self, "extend_selection", icon = "SELECT_EXTEND") + + def execute(self, context): + material_name = self.material_name + ext = self.extend_selection + return mu_select_by_material_name(self, material_name, ext) + + +class VIEW3D_OT_materialutilities_copy_material_to_others(bpy.types.Operator): + """Copy the material(s) of the active object to the other selected objects""" + + bl_idname = "view3d.materialutilities_copy_material_to_others" + bl_label = "Copy material(s) to others (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (context.active_object is not None) and (context.active_object.mode != 'EDIT') + + def execute(self, context): + return mu_copy_material_to_others(self) + + +class VIEW3D_OT_materialutilities_clean_material_slots(bpy.types.Operator): + """Removes any material slots from the selected objects that are not used""" + + bl_idname = "view3d.materialutilities_clean_material_slots" + bl_label = "Clean Material Slots (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return len(context.selected_editable_objects) > 0 + + def execute(self, context): + return mu_cleanmatslots(self) + + +class VIEW3D_OT_materialutilities_remove_material_slot(bpy.types.Operator): + """Remove the active material slot from selected object(s) + (See the operator panel [F9] for more options)""" + + bl_idname = "view3d.materialutilities_remove_material_slot" + bl_label = "Remove Active Material Slot (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + only_active: BoolProperty( + name = 'Only active object', + description = 'Only remove the active material slot for the active object ' + + '(otherwise do it for every selected object)' + ) + + @classmethod + def poll(cls, context): + return (context.active_object is not None) and (context.active_object.mode != 'EDIT') + + def draw(self, context): + layout = self.layout + layout.prop(self, "only_active", icon = "PIVOT_ACTIVE") + + def execute(self, context): + return mu_remove_material(self, self.only_active) + +class VIEW3D_OT_materialutilities_remove_all_material_slots(bpy.types.Operator): + """Remove all material slots from selected object(s) + (See the operator panel [F9] for more options)""" + + bl_idname = "view3d.materialutilities_remove_all_material_slots" + bl_label = "Remove All Material Slots (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + only_active: BoolProperty( + name = 'Only active object', + description = 'Only remove the material slots for the active object ' + + '(otherwise do it for every selected object)' + ) + + @classmethod + def poll(cls, context): + return (context.active_object is not None) and (context.active_object.mode != 'EDIT') + + def draw(self, context): + layout = self.layout + layout.prop(self, "only_active", icon = "PIVOT_ACTIVE") + + def execute(self, context): + return mu_remove_all_materials(self, self.only_active) + + +class VIEW3D_OT_materialutilities_replace_material(bpy.types.Operator): + """Replace a material by name""" + bl_idname = "view3d.materialutilities_replace_material" + bl_label = "Replace Material (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + matorg: StringProperty( + name = "Original", + description = "Material to find and replace", + maxlen = 63, + ) + matrep: StringProperty(name="Replacement", + description = "Material that will be used instead of the Original material", + maxlen = 63, + ) + all_objects: BoolProperty( + name = "All Objects", + description = "Replace for all objects in this blend file (otherwise only selected objects)", + default = True, + ) + update_selection: BoolProperty( + name = "Update Selection", + description = "Select affected objects and deselect unaffected", + default = True, + ) + + def draw(self, context): + layout = self.layout + + layout.prop_search(self, "matorg", bpy.data, "materials") + layout.prop_search(self, "matrep", bpy.data, "materials") + layout.separator() + + layout.prop(self, "all_objects", icon = "BLANK1") + layout.prop(self, "update_selection", icon = "SELECT_INTERSECT") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + return mu_replace_material(self.matorg, self.matrep, self.all_objects, self.update_selection) + + +class VIEW3D_OT_materialutilities_fake_user_set(bpy.types.Operator): + """Enable/disable fake user for materials""" + + bl_idname = "view3d.materialutilities_fake_user_set" + bl_label = "Set Fake User (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + fake_user: EnumProperty( + name = "Fake User", + description = "Turn fake user on or off", + items = mu_fake_user_set_enums, + default = 'TOGGLE' + ) + + materials: EnumProperty( + name = "Materials", + description = "Which materials of objects to affect", + items = mu_fake_user_materials_enums, + default = 'UNUSED' + ) + + @classmethod + def poll(cls, context): + return (context.active_object is not None) + + def draw(self, context): + layout = self.layout + layout.prop(self, "fake_user", expand = True) + layout.separator() + + layout.prop(self, "materials") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + return mu_set_fake_user(self, self.fake_user, self.materials) + + +class VIEW3D_OT_materialutilities_change_material_link(bpy.types.Operator): + """Link the materials to Data or Object, while keepng materials assigned""" + + bl_idname = "view3d.materialutilities_change_material_link" + bl_label = "Change Material Linking (Material Utilities)" + bl_options = {'REGISTER', 'UNDO'} + + override: BoolProperty( + name = "Override Data material", + description = "Override the materials assigned to the object data/mesh when switching to 'Linked to Data'\n" + + "(WARNING: This will override the materials of other linked objects, " + + "which have the materials linked to Data)", + default = False, + ) + link_to: EnumProperty( + name = "Link", + description = "What should the material be linked to", + items = mu_link_to_enums, + default = 'OBJECT' + ) + + affect: EnumProperty( + name = "Materials", + description = "Which materials of objects to affect", + items = mu_link_affect_enums, + default = 'SELECTED' + ) + + @classmethod + def poll(cls, context): + return (context.active_object is not None) + + def draw(self, context): + layout = self.layout + + layout.prop(self, "link_to", expand = True) + layout.separator() + + layout.prop(self, "affect") + layout.separator() + + layout.prop(self, "override", icon = "DECORATE_OVERRIDE") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + return mu_change_material_link(self, self.link_to, self.affect, self.override) + +class MATERIAL_OT_materialutilities_merge_base_names(bpy.types.Operator): + """Merges materials that has the same base names but ends with .xxx (.001, .002 etc)""" + + bl_idname = "material.materialutilities_merge_base_names" + bl_label = "Merge Base Names" + bl_description = "Merge materials that has the same base names but ends with .xxx (.001, .002 etc)" + + material_base_name: StringProperty( + name = "Material Base Name", + default = "", + description = 'Base name for materials to merge ' + + '(e.g. "Material" is the base name of "Material.001", "Material.002" etc.)' + ) + is_auto: BoolProperty( + name = "Auto Merge", + description = "Find all available duplicate materials and Merge them" + ) + + is_not_undo = False + material_error = [] # collect mat for warning messages + + + def replace_name(self): + """If the user chooses a material like 'Material.042', clean it up to get a base name ('Material')""" + + # use the chosen material as a base one, check if there is a name + self.check_no_name = (False if self.material_base_name in {""} else True) + + # No need to do this if it's already "clean" + # (Also lessens the potential of error given about the material with the Base name) + if '.' not in self.material_base_name: + return + + if self.check_no_name is True: + for mat in bpy.data.materials: + name = mat.name + + if name == self.material_base_name: + try: + base, suffix = name.rsplit('.', 1) + + # trigger the exception + num = int(suffix, 10) + self.material_base_name = base + mat.name = self.material_base_name + return + except ValueError: + if name not in self.material_error: + self.material_error.append(name) + return + + return + + def split_name(self, material): + """Split the material name into a base and a suffix""" + + name = material.name + + # No need to do this if it's already "clean"/there is no suffix + if '.' not in name: + return name, None + + base, suffix = name.rsplit('.', 1) + + try: + # trigger the exception + num = int(suffix, 10) + except ValueError: + # Not a numeric suffix + # Don't report on materials not actually included in the merge! + if ((self.is_auto or base == self.material_base_name) + and (name not in self.material_error)): + self.material_error.append(name) + return name, None + + if self.is_auto is False: + if base == self.material_base_name: + return base, suffix + else: + return name, None + + return base, suffix + + def fixup_slot(self, slot): + """Fix material slots that was assigned to materials now removed""" + + if not slot.material: + return + + base, suffix = self.split_name(slot.material) + if suffix is None: + return + + try: + base_mat = bpy.data.materials[base] + except KeyError: + print("\n[Materials Utilities Specials]\nLink to base names\nError:" + "Base material %r not found\n" % base) + return + + slot.material = base_mat + + def main_loop(self, context): + """Loops through all objects and material slots to make sure they are assigned to the right material""" + + for obj in context.scene.objects: + for slot in obj.material_slots: + self.fixup_slot(slot) + + @classmethod + def poll(self, context): + return len(context.selected_editable_objects) > 0 + + def draw(self, context): + layout = self.layout + + box_1 = layout.box() + box_1.prop_search(self, "material_base_name", bpy.data, "materials") + box_1.enabled = not self.is_auto + layout.separator() + + layout.prop(self, "is_auto", text = "Auto Rename/Replace", icon = "SYNTAX_ON") + + def invoke(self, context, event): + self.is_not_undo = True + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + # Reset Material errors, otherwise we risk reporting errors erroneously.. + self.material_error = [] + + if not self.is_auto: + self.replace_name() + + if self.check_no_name: + self.main_loop(context) + else: + self.report({'WARNING'}, "No Material Base Name given!") + + self.is_not_undo = False + return {'CANCELLED'} + + self.main_loop(context) + + if self.material_error: + materials = ", ".join(self.material_error) + + if len(self.material_error) == 1: + waswere = " was" + suff_s = "" + else: + waswere = " were" + suff_s = "s" + + self.report({'WARNING'}, materials + waswere + " not removed or set as Base" + suff_s) + + self.is_not_undo = False + return {'FINISHED'} + +class MATERIAL_OT_materialutilities_material_slot_move(bpy.types.Operator): + """Move the active material slot""" + + bl_idname = "material.materialutilities_slot_move" + bl_label = "Move Slot" + bl_description = "Move the material slot" + bl_options = {'REGISTER', 'UNDO'} + + movement: EnumProperty( + name = "Move", + description = "How to move the material slot", + items = mu_material_slot_move_enums + ) + + @classmethod + def poll(self, context): + # would prefer to access sely.movement here, but can'-'t.. + obj = context.active_object + if not obj: + return False + if (obj.active_material_index < 0) or (len(obj.material_slots) <= 1): + return False + return True + + def execute(self, context): + active_object = context.active_object + active_material = context.object.active_material + + if self.movement == 'TOP': + dir = 'UP' + + steps = active_object.active_material_index + else: + dir = 'DOWN' + + last_slot_index = len(active_object.material_slots) - 1 + steps = last_slot_index - active_object.active_material_index + + if steps == 0: + self.report({'WARNING'}, active_material.name + " already at " + self.movement.lower() + '!') + else: + for i in range(steps): + bpy.ops.object.material_slot_move(direction = dir) + + self.report({'INFO'}, active_material.name + ' moved to ' + self.movement.lower()) + + return {'FINISHED'} |