import bpy import json import os from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from .utils import convert_attr, get_addon_prefs ### --- Json serialized material load/save def load_palette(context, filepath): with open(filepath, 'r') as fd: mat_dic = json.load(fd) # from pprint import pprint # pprint(mat_dic) ob = context.object for mat_name, attrs in mat_dic.items(): curmat = bpy.data.materials.get(mat_name) if curmat:#exists if curmat.is_grease_pencil: if curmat not in ob.data.materials[:]:# add only if it's not already there ob.data.materials.append(curmat) continue else: mat_name = mat_name+'.01'#rename to avoid conflict ## to create a GP mat (from https://developer.blender.org/T67102) mat = bpy.data.materials.new(name=mat_name) bpy.data.materials.create_gpencil_data(mat)#cast to GP mat ob.data.materials.append(mat) for attr, value in attrs.items(): setattr(mat.grease_pencil, attr, value) class GPTB_OT_load_default_palette(bpy.types.Operator): bl_idname = "gp.load_default_palette" bl_label = "Load basic palette" bl_description = "Load a material palette on the current GP object\nif material name already exists in scene it will uses these" bl_options = {"REGISTER", "INTERNAL"} # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' def execute(self, context): # Start Clean (delete unuesed sh*t) bpy.ops.object.material_slot_remove_unused() #Rename default solid stroke if still there line = context.object.data.materials.get('Black') if line: line.name = 'line' if not line: line = context.object.data.materials.get('Solid Stroke') if line: line.name = 'line' # load json pfp = Path(bpy.path.abspath(get_addon_prefs().palette_path)) if not pfp.exists(): self.report({'ERROR'}, f'Palette path not found') return {"CANCELLED"} base = pfp / 'base.json' if not base.exists(): self.report({'ERROR'}, f'base.json palette not found in {pfp.as_posix()}') return {"CANCELLED"} load_palette(context, base) self.report({'INFO'}, f'Loaded base Palette') return {"FINISHED"} class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper): bl_idname = "gp.load_palette" bl_label = "Load palette" bl_description = "Load a material palette on the current GP object\nif material name already exists in scene it will uses these" #bl_options = {"REGISTER", "INTERNAL"} # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' filename_ext = '.json' filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'} )#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp filepath : bpy.props.StringProperty( name="File Path", description="File path used for import", maxlen= 1024) def execute(self, context): # load json load_palette(context, self.filepath) self.report({'INFO'}, f'settings loaded from: {os.path.basename(self.filepath)}') return {"FINISHED"} class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper): bl_idname = "gp.save_palette" bl_label = "save palette" bl_description = "Save a material palette from material on current GP object." #bl_options = {"REGISTER", "INTERNAL"} # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp filename_ext = '.json' filepath : bpy.props.StringProperty( name="File Path", description="File path used for export", maxlen= 1024) def execute(self, context): ob = context.object exclusions = ('bl_rna', 'rna_type') # save json dic = {} allmat=[] for mat in ob.data.materials: if not mat: continue if not mat.is_grease_pencil: continue if mat in allmat: continue allmat.append(mat) dic[mat.name] = {} for attr in dir(mat.grease_pencil): if attr.startswith('__'): continue if attr in exclusions: continue if mat.grease_pencil.bl_rna.properties[attr].is_readonly:#avoid readonly continue dic[mat.name][attr] = convert_attr(getattr(mat.grease_pencil, attr)) if not dic: self.report({'ERROR'}, f'No materials on this GP object') return {"CANCELLED"} # export with open(self.filepath, 'w') as fd: json.dump(dic, fd, indent='\t') self.report({'INFO'}, f'Palette saved: {self.filepath}')#WARNING, ERROR return {"FINISHED"} ### --- Direct material append/link from blend file def load_blend_palette(context, filepath): '''Load materials on current active object from current chosen blend''' print(f'-- import palette from : {filepath} --') for ob in context.selected_objects: if ob.type != 'GREASEPENCIL': print(f'{ob.name} not a GP object') continue print('\n', ob.name, ':') obj_mats = [m.name for m in ob.data.materials if m]# can found Nonetype scene_mats = [m.name for m in bpy.data.materials] # Link into the blend file with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): for name in data_from.materials: if name.lower() in ('bg', 'line', 'dots stroke'): continue if name in obj_mats: print(f"!- {name} already in object materials") continue if name in scene_mats: print(f'- {name} (found in scene)') ob.data.materials.append(bpy.data.materials[name]) continue ## TODO find a way to Update color !... complex... data_to.materials.append(name) if not data_to.materials: # print('Nothing to link/append from lib palette!') continue print('From palette append:') for mat in data_to.materials: print(f'- {mat.name}') ob.data.materials.append(mat) print(f'-- import Done --') ## list sources in a palette txt data block palette_txt = bpy.data.texts.get('palettes') if not palette_txt: palette_txt = bpy.data.texts.new('palettes') lines = [l.body for l in palette_txt.lines] if not os.path.basename(filepath) in lines: palette_txt.write('\n' + os.path.basename(filepath)) class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper): bl_idname = "gp.load_blend_palette" bl_label = "Load colo palette" bl_description = "Load a material palette from blend file on the current GP object\nif material name already exists in scene it will uses these" #bl_options = {"REGISTER", "INTERNAL"} # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' filename_ext = '.blend' filter_glob: bpy.props.StringProperty(default='*.blend', options={'HIDDEN'} )#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp filepath : bpy.props.StringProperty( name="File Path", description="File path used for import", maxlen= 1024) def execute(self, context): # load json load_blend_palette(context, self.filepath) self.report({'INFO'}, f'materials loaded from: {os.path.basename(self.filepath)}') return {"FINISHED"} class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator): bl_idname = "gp.copy_active_to_selected_palette" bl_label = "Append Materials To Selected" bl_description = "Copy all the materials of the active GP objects to the material stack of all the other selected GP" bl_options = {"REGISTER"} # , "INTERNAL" # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' def execute(self, context): ob = context.object if not len(ob.data.materials): self.report({'ERROR'}, 'No materials to transfer') return {"CANCELLED"} selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob] if not selection: self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials') return {"CANCELLED"} ct = 0 for o in selection: for mat in ob.data.materials: if mat in o.data.materials[:]: continue o.data.materials.append(mat) ct += 1 if ct: self.report({'INFO'}, f'{ct} Materials appended') else: self.report({'WARNING'}, 'All materials are already in other selected object') return {"FINISHED"} class GPTB_OT_clean_material_stack(bpy.types.Operator): bl_idname = "gp.clean_material_stack" bl_label = "Clean Material Stack" bl_description = "Clean materials duplication in active GP object stack" bl_options = {"REGISTER", "UNDO"} # , "INTERNAL" use_clean_mats : bpy.props.BoolProperty(name="Remove Duplication", description="All duplicated material (with suffix .001, .002 ...) will be replaced by the material with clean name (if found in scene)" , default=True) skip_different_materials : bpy.props.BoolProperty(name="Skip Different Material", description="Will not touch duplication if color settings are different (and show infos about skipped materials)", default=True) use_fuses_mats : bpy.props.BoolProperty(name="Fuse Materials Slots", description="Fuse materials slots when multiple uses same materials", default=True) remove_empty_slots : bpy.props.BoolProperty(name="Remove Empty Slots", description="Remove slots that haven't any material attached ", default=True) # skip_binded_empty_slots : bpy.props.BoolProperty(name="Skip Binded Empty slots", # description="Remove only empty slots that haven't any material attached", # default=False) @classmethod def poll(cls, context): return context.object and context.object.type == 'GREASEPENCIL' def invoke(self, context, event): self.ob = context.object return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout box = layout.box() box.prop(self, 'use_clean_mats') if self.use_clean_mats: box.prop(self, 'skip_different_materials') # layout.separator() box = layout.box() box.prop(self, 'use_fuses_mats') box = layout.box() box.prop(self, 'remove_empty_slots') # if self.remove_empty_slots: # box.prop(self, 'skip_binded_empty_slots') def different_gp_mat(self, mata, matb): a = mata.grease_pencil b = matb.grease_pencil if a.color[:] != b.color[:]: return f'! {self.ob.name}: {mata.name} and {matb.name} stroke color is different' if a.fill_color[:] != b.fill_color[:]: return f'! {self.ob.name}: {mata.name} and {matb.name} fill_color color is different' if a.show_stroke != b.show_stroke: return f'! {self.ob.name}: {mata.name} and {matb.name} stroke has different state' if a.show_fill != b.show_fill: return f'! {self.ob.name}: {mata.name} and {matb.name} fill has different state' ## Clean dups def clean_mats_duplication(self, ob): import re diff_ct = 0 todel = [] if ob.type != 'GREASEPENCIL': return if not hasattr(ob, 'material_slots'): return for i, ms in enumerate(ob.material_slots): mat = ms.material if not mat: continue match = re.search(r'(.*)\.\d{3}$', mat.name) if not match: continue basemat = bpy.data.materials.get(match.group(1)) if not basemat: continue diff = self.different_gp_mat(mat, basemat) if diff: print(diff) diff_ct += 1 if self.skip_different_materials: continue if mat not in todel: todel.append(mat) ms.material = basemat print(f'{ob.name} : slot {i} >> replaced {mat.name}') mat.use_fake_user = False ### delete (only when using on all objects loop, else can delete another objects mat...) ## for m in reversed(todel): ## bpy.data.materials.remove(m) if diff_ct: return('INFO', f'{diff_ct} mat skipped >> same name but different color settings!') ## fuse def fuse_object_mats(self, ob): for i in range(len(ob.material_slots))[::-1]: ms = ob.material_slots[i] mat = ms.material # if not mat: # # remove empty slots # if self.remove_empty_slots: # ob.active_material_index = i # bpy.ops.object.material_slot_remove() # continue # update mat list mlist = [ms.material for ms in ob.material_slots if ms.material] if mlist.count(mat) > 1: # get first material in list new_mat_id = mlist.index(mat) # iterate in all strokes and replace with new_mat_id for l in ob.data.layers: for f in l.frames: for s in f.drawing.strokes: if s.material_index == i: s.material_index = new_mat_id # delete slot (or add to the remove_slot list ob.active_material_index = i bpy.ops.object.material_slot_remove() def delete_empty_material_slots(self, ob): for i in range(len(ob.material_slots))[::-1]: ms = ob.material_slots[i] mat = ms.material if not mat: # is_binded=False # if self.skip_binded_empty_slots: # for l in ob.data.layers: # for f in l.frames: # for s in f.drawing.strokes: # if s.material_index == i: # is_binded = True # break # if is_binded: # continue ob.active_material_index = i bpy.ops.object.material_slot_remove() def execute(self, context): ob = context.object info = None if not self.use_clean_mats and not self.use_fuses_mats and not self.remove_empty_slots: self.report({'ERROR'}, 'At least one operation should be selected') return {"CANCELLED"} if self.use_clean_mats: info = self.clean_mats_duplication(ob) if self.use_fuses_mats: self.fuse_object_mats(ob) if self.remove_empty_slots: self.delete_empty_material_slots(ob) if info: self.report({info[0]}, info[1]) # else: # self.report({'WARNING'}, '') return {"FINISHED"} classes = ( GPTB_OT_load_palette, GPTB_OT_save_palette, GPTB_OT_load_default_palette, GPTB_OT_load_blend_palette, GPTB_OT_copy_active_to_selected_palette, GPTB_OT_clean_material_stack, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)