Part 1 - The setup
This is the first step towards a material library loader for cycles node view. In the following scenario imagine you have a folder inside /scripts called addons_contrib/io_material_loader. io_material_loader will contain a .blend called practicality.blend. That blend will contain a material called 'vibrant'.
The import material function takes a known name of a material and a known name of a blend and looks in that blend for the material. It performs no error checking in this scenario, mainly because eventually the function will only be called with materials that I(or the script) know are present.
There are some drawbacks to this approach as it relies on ops, (insert clarification later). But for now it will suffice to act as example.
Now the decision is where to add the menu then, in nodeview properties panel? Uch... if i must, albeit just to have a working prototype - so i can use it. This leaves me to settle on the uglier flat list, because doing an eval for creating menus is not an idea i like to think about. This is my current (lame) solution, so i can focus on combining parts 1 and 2. clicky
What follows is the combination of all parts:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy | |
import os | |
def import_material(matname): | |
from os.path import join | |
path_to_scripts = bpy.utils.script_paths()[0] | |
path_to_script = join(path_to_scripts, 'addons_contrib', 'io_material_loader') | |
material_locator = "\\Material\\" | |
file_name = "practicality.blend" | |
opath = "//" + file_name + material_locator + matname | |
dpath = join(path_to_script, file_name) + material_locator | |
bpy.ops.wm.link_append( | |
filepath=opath, # "//filename.blend\\Folder\\" | |
filename=matname, # "material_name | |
directory=dpath, # "fullpath + \\Folder\\ | |
filemode=1, | |
link=False, | |
autoselect=False, | |
active_layer=True, | |
instance_groups=False, | |
relative_path=True) | |
import_material("vibrant") |
There are some drawbacks to this approach as it relies on ops, (insert clarification later). But for now it will suffice to act as example.
Part 2 - Reading the material content of a .blend
This will allow the script to populate a menu with the materials present in a blend file. It will rely on the methods used by ideasman_42 in this gist : here. My modification step which is viewable here takes a large chunk of that script and prints the names of blends located inside addons_contrib/io_material_loader. The next step below will make more sense now. It should print filenames and material names contained therein.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy | |
import os | |
from os.path import join | |
def get_source_files(): | |
path_to_files = bpy.utils.script_paths()[0] | |
DIR = join(path_to_files, 'addons_contrib', 'io_material_loader') | |
source_files = [] | |
source_files.extend(source_list(DIR, filename_check=lambda f:f.endswith(".blend"))) | |
return source_files | |
def source_list(path, filename_check=None): | |
for dirpath, dirnames, filenames in os.walk(path): | |
# skip '.svn' | |
if dirpath.startswith("."): | |
continue | |
for filename in filenames: | |
filepath = join(dirpath, filename) | |
if filename_check is None or filename_check(filepath): | |
yield filepath | |
def print_materials_and_blendnames(source_files): | |
print("-"*40) | |
for i, filepath in enumerate(source_files): | |
# print only filename | |
file_name = filepath.split(os.sep)[-1] | |
print(file_name, i+1, "of", len(source_files)) | |
# print list of materials with users, if present. | |
with bpy.data.libraries.load(filepath) as (data_from, data_to): | |
if data_from.materials: | |
for material in data_from.materials: | |
print("\t-"+ material) | |
source_files = get_source_files() | |
print_materials_and_blendnames(source_files) |
Part 3 - ditching part 1
Now we have the basic code soup required to get the job done. After having spent the best part of a day trying to pass a property along with layout.operator_menu_enum( ).some_property = some_value, which seems like an utterly trivial thing to do, it appears that the API doesn't provide this functionality. I died a little inside as ideasman_42 confirmed that it probably wasn't a feature of the API. If you would like to see an example of how I had hoped to implement the menu ( example script, displays new menu in nodeview ). Perhaps there is still a neat solution, but i've exhausted my desire to find a solution to the first idea.Now the decision is where to add the menu then, in nodeview properties panel? Uch... if i must, albeit just to have a working prototype - so i can use it. This leaves me to settle on the uglier flat list, because doing an eval for creating menus is not an idea i like to think about. This is my current (lame) solution, so i can focus on combining parts 1 and 2. clicky
What follows is the combination of all parts:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import bpy | |
from bpy.props import StringProperty | |
from os.path import join | |
from collections import OrderedDict | |
# helper constant strings | |
split_token = "<--->" | |
material_locator = "\\Material\\" | |
locator_prefix = "//" | |
path_to_scripts = bpy.utils.script_paths()[0] | |
path_to_script = join(path_to_scripts, 'addons_contrib', 'io_material_loader') | |
# helper functions | |
def import_material(file_name, matname): | |
""" | |
takes input file_name, and the material name located within, and imports into | |
current .blend | |
""" | |
file_name += '.blend' | |
opath = locator_prefix + file_name + material_locator + matname | |
dpath = join(path_to_script, file_name) + material_locator | |
bpy.ops.wm.link_append( | |
filepath=opath, # "//filename.blend\\Folder\\" | |
filename=matname, # "material_name | |
directory=dpath, # "fullpath + \\Folder\\ | |
filemode=1, | |
link=False, | |
autoselect=False, | |
active_layer=True, | |
instance_groups=False, | |
relative_path=True) | |
def get_source_files(): | |
""" | |
given the predefined place to look for materials, this populates source_files | |
with the list of .blends inside the directory pointed at. | |
""" | |
source_files = [] | |
source_files.extend(source_list(path_to_script, | |
filename_check=lambda f:f.endswith(".blend"))) | |
return source_files | |
def source_list(path, filename_check=None): | |
""" | |
generates the iterable used to find all .blend files. | |
""" | |
for dirpath, dirnames, filenames in os.walk(path): | |
# skip hidden dirs | |
if dirpath.startswith("."): | |
continue | |
for filename in filenames: | |
# skip non .blend | |
if not filename.endswith('.blend'): | |
continue | |
# we want these | |
filepath = join(dirpath, filename) | |
if filename_check is None or filename_check(filepath): | |
yield filepath | |
def get_materials_dict(): | |
""" | |
Generates an ordered dictionary to map materials to the blend files that hold them. | |
""" | |
source_files = get_source_files() | |
mlist = OrderedDict() | |
for i, filepath in enumerate(source_files): | |
file_name = filepath.split(os.sep)[-1].replace('.blend', '') | |
with bpy.data.libraries.load(filepath) as (data_from, data_to): | |
if data_from.materials: | |
materials_in_file = [mat for mat in data_from.materials] | |
mlist[file_name] = materials_in_file | |
return mlist | |
class AddMaterial(bpy.types.Operator): | |
bl_idname = "scene.add_material_operator" | |
bl_label = "Add Material Operator" | |
selection = StringProperty() | |
def execute(self, context): | |
file_name, file_material = self.selection.split(split_token) | |
import_material(file_name, file_material) | |
print(file_name, file_material) | |
return {'FINISHED'} | |
class CustomMenu(bpy.types.Menu): | |
bl_label = "Materials" | |
bl_idname = "OBJECT_MT_custom_menu" | |
my_mat_dict = get_materials_dict() | |
def draw(self, context): | |
layout = self.layout | |
for key in self.my_mat_dict: | |
for material_choice in self.my_mat_dict[key]: | |
info = key + split_token + material_choice | |
layout.operator( 'scene.add_material_operator', | |
text=material_choice).selection = info | |
layout.label(text=key, icon='MATERIAL') | |
def draw_item(self, context): | |
layout = self.layout | |
layout.menu(CustomMenu.bl_idname) | |
def register(): | |
bpy.utils.register_class(AddMaterial) | |
bpy.utils.register_class(CustomMenu) | |
bpy.types.NODE_HT_header.append(draw_item) | |
def unregister(): | |
bpy.utils.unregister_class(AddMaterial) | |
bpy.utils.unregister_class(CustomMenu) | |
bpy.types.NODE_HT_header.remove(draw_item) | |
if __name__ == "__main__": | |
register() |