New material cleaner
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
This commit is contained in:
		
							parent
							
								
									f10f572bdc
								
							
						
					
					
						commit
						94e3b7a7ad
					
				@ -1,6 +1,13 @@
 | 
				
			|||||||
# Changelog
 | 
					# 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
 | 
					1.4.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- fix: custom passepartout size limit when dezooming in camera
 | 
					- fix: custom passepartout size limit when dezooming in camera
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										180
									
								
								OP_palettes.py
									
									
									
									
									
								
							
							
						
						
									
										180
									
								
								OP_palettes.py
									
									
									
									
									
								
							@ -282,12 +282,192 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return {"FINISHED"}
 | 
					        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 = (
 | 
					classes = (
 | 
				
			||||||
GPTB_OT_load_palette,
 | 
					GPTB_OT_load_palette,
 | 
				
			||||||
GPTB_OT_save_palette,
 | 
					GPTB_OT_save_palette,
 | 
				
			||||||
GPTB_OT_load_default_palette,
 | 
					GPTB_OT_load_default_palette,
 | 
				
			||||||
GPTB_OT_load_blend_palette,
 | 
					GPTB_OT_load_blend_palette,
 | 
				
			||||||
GPTB_OT_copy_active_to_selected_palette,
 | 
					GPTB_OT_copy_active_to_selected_palette,
 | 
				
			||||||
 | 
					GPTB_OT_clean_material_stack,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def register():
 | 
					def register():
 | 
				
			||||||
 | 
				
			|||||||
@ -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.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.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.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 = (
 | 
					classes = (
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ bl_info = {
 | 
				
			|||||||
"name": "GP toolbox",
 | 
					"name": "GP toolbox",
 | 
				
			||||||
"description": "Set of tools for Grease Pencil in animation production",
 | 
					"description": "Set of tools for Grease Pencil in animation production",
 | 
				
			||||||
"author": "Samuel Bernou",
 | 
					"author": "Samuel Bernou",
 | 
				
			||||||
"version": (1, 4, 1),
 | 
					"version": (1, 4, 2),
 | 
				
			||||||
"blender": (2, 91, 0),
 | 
					"blender": (2, 91, 0),
 | 
				
			||||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
 | 
					"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
 | 
				
			||||||
"warning": "",
 | 
					"warning": "",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user