diff --git a/CHANGELOG.md b/CHANGELOG.md index bb30828..a6ede00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog +1.4.2 + +- feat: new material cleaner in GP layer menu with 3 options + - clean material duplication (with sub option to not clear if color settings are different) + - fuse material slots that have the same materials + - remove empty slots + 1.4.1 - fix: custom passepartout size limit when dezooming in camera diff --git a/OP_palettes.py b/OP_palettes.py index 9f56a3b..81f917e 100644 --- a/OP_palettes.py +++ b/OP_palettes.py @@ -282,12 +282,192 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator): 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 == 'GPENCIL' + + 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 != 'GPENCIL': + 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"} + + 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(): diff --git a/UI_tools.py b/UI_tools.py index 2636773..863a05d 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -351,6 +351,7 @@ def palette_manager_menu(self, context): layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path layout.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path + layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL') classes = ( diff --git a/__init__.py b/__init__.py index b3ac276..c52bc78 100644 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou", -"version": (1, 4, 1), +"version": (1, 4, 2), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "",