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 == 'GPENCIL' 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 == 'GPENCIL' 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 == 'GPENCIL' 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.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''' #from pathlib import Path #palette_fp = C.preferences.addons['GP_toolbox'].preferences['palette_path'] #fp = Path(palette_fp) / 'christina.blend' print(f'-- import palette from : {filepath} --') for ob in context.selected_objects: if ob.type != 'GPENCIL': 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 == 'GPENCIL' 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"} classes = ( GPTB_OT_load_palette, GPTB_OT_save_palette, GPTB_OT_load_default_palette, GPTB_OT_load_blend_palette, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)