materials_utils: return to release: T67990 T63750 01d80b8f60
This commit is contained in:
parent
1bba102d65
commit
802904cf43
|
@ -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()
|
|
@ -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"))
|
|
@ -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'}
|
|
@ -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')
|
|
@ -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'}
|
Loading…
Reference in New Issue