2021-01-10 16:47:17 +01:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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
|
2021-12-04 13:57:32 +01:00
|
|
|
pfp = Path(bpy.path.abspath(get_addon_prefs().palette_path))
|
2021-01-10 16:47:17 +01:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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:
|
2023-09-27 14:54:56 +02:00
|
|
|
if not mat:
|
|
|
|
continue
|
2021-01-10 16:47:17 +01:00
|
|
|
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'''
|
2021-12-04 13:57:32 +01:00
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
print(f'-- import palette from : {filepath} --')
|
|
|
|
for ob in context.selected_objects:
|
2024-11-11 15:35:39 +01:00
|
|
|
if ob.type != 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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"}
|
|
|
|
|
2021-03-16 22:52:26 +01:00
|
|
|
|
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-03-16 22:52:26 +01:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
ob = context.object
|
|
|
|
if not len(ob.data.materials):
|
|
|
|
self.report({'ERROR'}, 'No materials to transfer')
|
|
|
|
return {"CANCELLED"}
|
|
|
|
|
2024-11-11 15:35:39 +01:00
|
|
|
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
|
2021-03-16 22:52:26 +01:00
|
|
|
|
|
|
|
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"}
|
|
|
|
|
2021-06-07 19:07:37 +02:00
|
|
|
|
|
|
|
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):
|
2024-11-11 15:35:39 +01:00
|
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
2021-06-07 19:07:37 +02:00
|
|
|
|
|
|
|
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 = []
|
2024-11-11 15:35:39 +01:00
|
|
|
if ob.type != 'GREASEPENCIL':
|
2021-06-07 19:07:37 +02:00
|
|
|
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.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.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"}
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
classes = (
|
|
|
|
GPTB_OT_load_palette,
|
|
|
|
GPTB_OT_save_palette,
|
|
|
|
GPTB_OT_load_default_palette,
|
|
|
|
GPTB_OT_load_blend_palette,
|
2021-03-16 22:52:26 +01:00
|
|
|
GPTB_OT_copy_active_to_selected_palette,
|
2021-06-07 19:07:37 +02:00
|
|
|
GPTB_OT_clean_material_stack,
|
2021-11-25 17:00:09 +01:00
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def register():
|
|
|
|
for cls in classes:
|
|
|
|
bpy.utils.register_class(cls)
|
|
|
|
|
|
|
|
def unregister():
|
|
|
|
for cls in reversed(classes):
|
|
|
|
bpy.utils.unregister_class(cls)
|