1.7.0 - fix: problem when removing render layers - changed: node distribution refactor, allow separate compositing scene - Compositing scene (holding nodes) can be separated from render scene (holding GP objects and related viewlayers) - Default render named changed from `Render` to `RenderGP` - New properties in exposed Dopesheet N panel to manually set Render scene and Compo scene - Operator expose a `node_scene` parameter to separate where to send nodes - Switch scene button can have an extra button to go in compo scene if found
		
			
				
	
	
		
			654 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			654 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import bpy
 | |
| from bpy.props import (FloatProperty,
 | |
|                         BoolProperty,
 | |
|                         EnumProperty,
 | |
|                         StringProperty,
 | |
|                         IntProperty)
 | |
| from . import fn
 | |
| import math
 | |
| import re
 | |
| import json
 | |
| from pathlib import Path
 | |
| 
 | |
| ## TODO : export json info to re-setup layers in AE automagically
 | |
| 
 | |
| def check_outname(ob, l):
 | |
|     vl_name = l.viewlayer_render
 | |
|     if vl_name in {'exclude', 'View Layer'}:
 | |
|         return
 | |
| 
 | |
|     ng_name = f'NG_{ob.name}'
 | |
|     ## check in wich node tree this exists
 | |
|     for scn in bpy.data.scenes:
 | |
|         if scn.name == 'Scene':
 | |
|             continue
 | |
|         ng = scn.node_tree.nodes.get(ng_name)
 | |
|         if ng:
 | |
|             break
 | |
|     print(scn.name)
 | |
|     if not ng:
 | |
|         print(f'Skip {vl_name}: Not found nodegroup {ng_name}' )
 | |
|         return
 | |
|     ng_socket = ng.outputs.get(vl_name)
 | |
|     if not ng_socket:
 | |
|         print(f'Skip {vl_name}: Not found in nodegroup {ng_name} sockets')
 | |
|         return
 | |
|     if not len(ng_socket.links):
 | |
|         print(f' socket is disconnected in {ng_name} nodegroup')
 | |
|         return
 | |
|     fo_node = ng_socket.links[0].to_node
 | |
|     fo_socket = ng_socket.links[0].to_socket
 | |
|     if fo_node.type != 'OUTPUT_FILE':
 | |
|         print(f'Skip {vl_name}: node is not an output_file {fo_node.name}')
 | |
|         return
 | |
| 
 | |
|     # fo_socket.name isn't right, have to iterate in paths
 | |
|     idx = [i for i in fo_node.inputs].index(fo_socket)
 | |
|     subpath = fo_node.file_slots[idx].path
 | |
|     # fp = Path(fo_node.base_path.rstrip('/')) / subpath
 | |
|     # fp = Path(bpy.path.abspath(str(fp)).rstrip("/")) # abspath on disk
 | |
|     outname = subpath.split('/')[0] # folder name on disk
 | |
| 
 | |
|     return outname
 | |
| 
 | |
| class GPEXP_OT_export_infos_for_compo(bpy.types.Operator):
 | |
|     bl_idname = "gp.export_infos_for_compo"
 | |
|     bl_label = "Export Infos For Compo"
 | |
|     bl_description = "Export informations for compositing, including layers with masks, fusion mode, opacity"
 | |
|     bl_options = {"REGISTER"}
 | |
| 
 | |
|     # @classmethod
 | |
|     # def poll(cls, context):
 | |
|     #     return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     skip_check : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         self.l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
 | |
|         if self.skip_check:
 | |
|             if self.l_infos.exists():
 | |
|                 # Preferably skip if already user defined ?
 | |
|                 return {'FINISHED'}
 | |
|             return self.execute(context)
 | |
| 
 | |
|         if self.l_infos.exists():
 | |
|             return context.window_manager.invoke_props_dialog(self)
 | |
|         return self.execute(context)
 | |
| 
 | |
|     def draw(self, context):
 | |
|         layout = self.layout
 | |
|         layout.label(text='An infos Json already exists', icon = 'ERROR')
 | |
|         layout.label(text='Do you want to overwrite ?')
 | |
|         layout.label(text='Note: Must export before "Check Layers" step', icon='INFO')
 | |
| 
 | |
|     def execute(self, context):
 | |
|         ## Repeat because might not be registered if called with invoke_default
 | |
|         self.l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
 | |
|         dic = {}
 | |
|         pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|         for o in pool:
 | |
|             # if not o.visible_get():
 | |
|             #     continue
 | |
|             for l in o.data.layers:
 | |
|                 # skip non rendered layers
 | |
|                 if l.hide:
 | |
|                     continue
 | |
| 
 | |
|                 if l.info.startswith('MA_'):
 | |
|                     # No point in storing information of masking layers...
 | |
|                     continue
 | |
| 
 | |
|                 ## Can't check viewlayers and final fileout name if Render scene not even created...
 | |
|                 """ if not l.viewlayer_render or l.viewlayer_render == 'exclude':
 | |
|                     continue
 | |
| 
 | |
|                 fo_name = check_outname(o, l) # get name used for output file folder (same in AE)
 | |
|                 if not fo_name:
 | |
|                     print(f'! Could not found fileout name for {o.name} > {l.info}')
 | |
|                     continue
 | |
|                 """
 | |
| 
 | |
|                 ldic = {}
 | |
|                 ## Check opacity, blend mode
 | |
|                 if l.opacity < 1.0:
 | |
|                     ldic['opacity'] = l.opacity
 | |
| 
 | |
|                 if l.blend_mode != 'REGULAR':
 | |
|                     ldic['blend_mode'] = l.blend_mode
 | |
| 
 | |
|                 if l.use_mask_layer:
 | |
| 
 | |
|                     multi_mask = {}
 | |
| 
 | |
|                     ## dict key as number for masks
 | |
|                     # for i, ml in enumerate(l.mask_layers):
 | |
|                     #     mask = {}
 | |
|                     #     if ml.hide:
 | |
|                     #         continue
 | |
|                     #     mask['name'] = ml.name
 | |
|                     #     if ml.invert: # create key get only if inverted
 | |
|                     #         mask['invert'] = ml.invert
 | |
|                     #     # multi_mask[ml.name] = mask
 | |
|                     #     multi_mask[i] = mask
 | |
| 
 | |
|                     ## dict key as mask name
 | |
|                     for ml in l.mask_layers:
 | |
|                         mask = {}
 | |
|                         if ml.hide:
 | |
|                             continue
 | |
|                         # mask['name'] = ml.name
 | |
|                         if ml.invert: # create key get only if inverted
 | |
|                             mask['invert'] = ml.invert # ! no key if no invert
 | |
|                         multi_mask[ml.name] = mask
 | |
| 
 | |
|                     if multi_mask:
 | |
|                         ldic['masks'] = multi_mask
 | |
| 
 | |
|                 ## add to full dic
 | |
|                 if ldic:
 | |
|                     # add source object ? might be usefull to pin point layer
 | |
|                     ldic['object'] = o.name
 | |
|                     dic[fn.normalize_layer_name(l, get_only=True)] = ldic
 | |
| 
 | |
|         if dic:
 | |
|             self.l_infos.parent.mkdir(exist_ok=True) # create render folder if needed
 | |
|             with self.l_infos.open('w') as fd:
 | |
|                 json.dump(dic, fd, indent='\t')
 | |
|             self.report({'INFO'}, f'Exported json at: {self.l_infos.as_posix()}')
 | |
|         else:
 | |
|             self.report({'WARNING'}, f'No custom data to write with those objects/layers')
 | |
|             return {"CANCELLED"}
 | |
| 
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| class GPEXP_OT_layers_state(bpy.types.Operator):
 | |
|     bl_idname = "gp.layers_state"
 | |
|     bl_label = "Set Layers State"
 | |
|     bl_description = "Display state of layer that migh need adjustement"
 | |
|     bl_options = {"REGISTER"} # , "UNDO"
 | |
| 
 | |
|     # clear_unused_view_layers :BoolProperty(name="Clear unused view layers",
 | |
|     #     description="Delete view layer that aren't used in the nodetree anymore",
 | |
|     #     default=True)
 | |
| 
 | |
|     all_objects : BoolProperty(name='On All Object',
 | |
|         default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     set_full_opacity : BoolProperty(name='Set Full Opacity',
 | |
|         default=True, description='Check/Set full opacity') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     set_use_lights : BoolProperty(name='Disable Use Light',
 | |
|         default=True, description='Check/Set use lights disabling') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     set_blend_mode : BoolProperty(name='Set Regular Blend Mode',
 | |
|         default=True, description='Check/Set blend mode to regular') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     clear_frame_out_of_range : BoolProperty(name='Clear Frames Out Of Scene Range',
 | |
|         default=False, description='Delete frames that before scene start and after scene end range\
 | |
|             \nWith a tolerance of one frame to avoid problem\
 | |
|             \nAffect all layers)') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     opacity_exclude_list : StringProperty(name='Skip',
 | |
|         default='MA', description='Skip prefixes from this list when changing opacity\
 | |
|             \nSeparate multiple value with a comma (ex: MAIN)') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     hide_invisible_materials : BoolProperty(name='Hide Materials named invisible',
 | |
|         default=True, description='Hide material with name starting with "invisible"') # , options={'SKIP_SAVE'}
 | |
| 
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
| 
 | |
|         ## if no existing infos.json generated, call ops
 | |
|         l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
 | |
|         if not l_infos.exists(): # only if infos not created
 | |
|             bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT')
 | |
| 
 | |
|         # return self.execute(context)
 | |
|         return context.window_manager.invoke_props_dialog(self)
 | |
| 
 | |
|     def draw(self, context):
 | |
|         layout = self.layout
 | |
|         layout.prop(self, 'all_objects')
 | |
|         total = len([o for o in context.scene.objects if o.type == 'GPENCIL'])
 | |
| 
 | |
|         target_num = total if self.all_objects else len([o for o in context.selected_objects if o.type == 'GPENCIL'])
 | |
|         layout.label(text=f'{target_num}/{total} targeted GP')
 | |
| 
 | |
|         layout.separator()
 | |
|         layout.prop(self, 'clear_frame_out_of_range')
 | |
|         layout.separator()
 | |
|         layout.label(text='Set (or only perform a check):')
 | |
|         row = layout.row()
 | |
|         row.prop(self, 'set_full_opacity')
 | |
|         if self.set_full_opacity:
 | |
|             row.prop(self, 'opacity_exclude_list')
 | |
|         layout.prop(self, 'set_use_lights')
 | |
|         layout.prop(self, 'set_blend_mode')
 | |
|         layout.prop(self, 'hide_invisible_materials')
 | |
|         # layout.prop(self, 'clear_unused_view_layers')
 | |
| 
 | |
|     def execute(self, context):
 | |
|         if self.all_objects:
 | |
|             pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|         else:
 | |
|             pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
 | |
|             # pool = [context.object]
 | |
| 
 | |
|         changes = []
 | |
|         for ob in pool:
 | |
|             changes.append(f'>> {ob.name}')
 | |
|             layers = ob.data.layers
 | |
| 
 | |
|             if self.clear_frame_out_of_range:
 | |
|                 ct = fn.clear_frame_out_of_range(ob, verbose=False)
 | |
|                 if ct:
 | |
|                     changes.append(f'{ct} out of range frame deleted')
 | |
| 
 | |
|             for l in layers:
 | |
|                 used = False
 | |
| 
 | |
|                 ## mask check
 | |
|                 # if l.mask_layers:
 | |
|                 #     print(f'-> masks')
 | |
|                 #     state = '' if l.use_mask_layer else ' (disabled)'
 | |
|                 #     print(f'{ob.name} > {l.info}{state}:')
 | |
|                 #     used = True
 | |
|                 #     for ml in l.mask_layers:
 | |
|                 #         mlstate = ' (disabled)' if ml.hide else ''
 | |
|                 #         mlinvert = ' <>' if ml.invert else ''
 | |
|                 #         print(f'{ml.info}{mlstate}{mlinvert}')
 | |
| 
 | |
| 
 | |
|                 if l.opacity != 1:
 | |
|                     # TODO Skip zeroed opacity ?
 | |
|                     # check if there is an exclusion word
 | |
|                     if any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x):
 | |
|                         print(f'Skipped layer : {l.info}')
 | |
|                     else:
 | |
|                         full_opacity_state = '' if self.set_full_opacity else ' (check only)'
 | |
|                         mess = f'{l.info} : opacity {l.opacity:.2f} >> 1.0{full_opacity_state}'
 | |
|                         print(mess)
 | |
|                         changes.append(mess)
 | |
|                         if self.set_full_opacity:
 | |
|                             l.opacity = 1.0
 | |
|                         used = True
 | |
| 
 | |
|                 if l.use_lights:
 | |
|                     use_lights_state = '' if self.set_use_lights else ' (check only)'
 | |
|                     mess = f'{l.info} : disable use lights{use_lights_state}'
 | |
|                     print(mess)
 | |
|                     # changes.append(mess) # don't report disable use_light... too many messages
 | |
|                     if self.set_use_lights:
 | |
|                         l.use_lights = False
 | |
|                     used = True
 | |
| 
 | |
|                 if l.blend_mode != 'REGULAR':
 | |
|                     blend_mode_state = '' if self.set_blend_mode else ' (check only)'
 | |
|                     mess = f'{l.info} : blend mode "{l.blend_mode}" >> regular{blend_mode_state}'
 | |
|                     print(mess)
 | |
|                     changes.append(mess)
 | |
|                     if self.set_blend_mode:
 | |
|                         l.blend_mode = 'REGULAR'
 | |
|                     used = True
 | |
| 
 | |
|                 if len(l.frames) == 1 and len(l.frames[0].strokes) == 0 and not l.hide:
 | |
|                     # probably used as separator
 | |
|                     l.hide = True
 | |
|                     mess = f'{l.info} : No frames. Hiding layer'
 | |
|                     print(mess)
 | |
|                     changes.append(mess)
 | |
|                     used = True
 | |
| 
 | |
|                 if used:
 | |
|                     print()
 | |
|             if changes:
 | |
|                 changes.append('')
 | |
| 
 | |
|         ## disable multiframe editing on all GP (can cause artifacts on render)
 | |
|         gp_mu_edit_ct = 0
 | |
|         for gp in bpy.data.grease_pencils:
 | |
|             if gp.use_multiedit:
 | |
|                 print(f'Disabling multi-edit on GP {gp.name}')
 | |
|                 gp.use_multiedit = False
 | |
|                 gp_mu_edit_ct += 1
 | |
| 
 | |
|         if gp_mu_edit_ct:
 | |
|             changes.append(f'{gp_mu_edit_ct} multiframe-edit mode disabled')
 | |
| 
 | |
|         ## Hide invisible named materials
 | |
|         if self.hide_invisible_materials:
 | |
|             for m in bpy.data.materials:
 | |
|                 if m.is_grease_pencil and m.name.lower().startswith('invisible'):
 | |
|                     if not m.grease_pencil.hide:
 | |
|                         print(f'Hiding gp material {m.name}')
 | |
|                         m.grease_pencil.hide = True
 | |
|                         changes.append(f'{m.name} material hidden')
 | |
| 
 | |
|         fn.show_message_box(_message=changes, _title="Layers Check Report", _icon='INFO')
 | |
| 
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| class GPEXP_OT_lower_layers_name(bpy.types.Operator):
 | |
|     bl_idname = "gp.lower_layers_name"
 | |
|     bl_label = "Normalize Layers Name"
 | |
|     bl_description = "Make the object and layers name lowercase with dashed converted to underscore (without touching layer prefix and suffix)"
 | |
|     bl_options = {"REGISTER", "UNDO"}
 | |
| 
 | |
|     # @classmethod
 | |
|     # def poll(cls, context):
 | |
|     #     return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     all_objects : BoolProperty(name='On All Object',
 | |
|         default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     object_name : BoolProperty(name='Normalize Object Name',
 | |
|         default=True, description='Make the object name lowercase') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     layer_name : BoolProperty(name='Normalize Layers Names',
 | |
|         default=True, description='Make the layers name lowercase') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     # dash_to_undescore : BoolProperty(name='Dash To Underscore',
 | |
|     #     default=True, description='Make the layers name lowercase') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         # self.ctrl=event.ctrl
 | |
|         # self.alt=event.alt
 | |
|         if event.alt:
 | |
|             self.all_objects=True
 | |
|         # return self.execute(context)
 | |
|         return context.window_manager.invoke_props_dialog(self)
 | |
| 
 | |
|     def draw(self, context):
 | |
|         layout = self.layout
 | |
|         layout.prop(self, 'all_objects')
 | |
|         if self.all_objects:
 | |
|             gp_ct = len([o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)])
 | |
|         else:
 | |
|             gp_ct = len([o for o in context.selected_objects if o.type == 'GPENCIL'])
 | |
| 
 | |
|         layout.label(text=f'{gp_ct} to lower-case')
 | |
|         layout.separator()
 | |
|         layout.label(text=f'Choose what to rename:')
 | |
|         layout.prop(self, 'object_name')
 | |
|         layout.prop(self, 'layer_name')
 | |
|         # if self.layer_name:
 | |
|         #     box = layout.box()
 | |
|         #     box.prop(self, 'dash_to_undescore')
 | |
| 
 | |
|         if not self.object_name and not self.layer_name:
 | |
|             layout.label(text=f'At least one choice!', icon='ERROR')
 | |
| 
 | |
|     def execute(self, context):
 | |
|         if self.all_objects:
 | |
|             pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|         else:
 | |
|             pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
 | |
| 
 | |
|         for ob in pool:
 | |
| 
 | |
|             if self.object_name:
 | |
|                 rename_data = ob.name == ob.data.name
 | |
|                 ob.name = ob.name.lower().replace('-', '_')
 | |
|                 if rename_data:
 | |
|                     ob.data.name = ob.name
 | |
| 
 | |
|             if self.layer_name:
 | |
|                 for l in ob.data.layers:
 | |
|                     # if self.dash_to_undescore:
 | |
|                     l.info = l.info.replace('-', '_')
 | |
|                     fn.normalize_layer_name(l) # default : lower=True, dash_to_underscore=self.dash_to_undescore
 | |
| 
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| 
 | |
| class GPEXP_OT_auto_number_object(bpy.types.Operator):
 | |
|     bl_idname = "gp.auto_number_object"
 | |
|     bl_label = "Auto Number Object"
 | |
|     bl_description = "Automatic prefix number based on origin distance to camera and in_front values\nCtrl + Clic to delete name to delete numbering"
 | |
|     bl_options = {"REGISTER", "UNDO"}
 | |
| 
 | |
|     # @classmethod
 | |
|     # def poll(cls, context):
 | |
|     #     return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     all_objects : BoolProperty(name='On All GP Object',
 | |
|         default=True, description='On All object, else use selected Grease Pencil objects') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     rename_data : BoolProperty(name='Rename Gpencil Data',
 | |
|         default=True, description='Rename Also the Grease Pencil data using same name as object') # , options={'SKIP_SAVE'}
 | |
| 
 | |
|     delete : BoolProperty(default=False, options={'SKIP_SAVE'})
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         # if event.alt:
 | |
|         #     self.all_objects=True
 | |
|         if event.ctrl or self.delete:
 | |
|             regex_num = re.compile(r'^(\d{3})_')
 | |
|             ct = 0
 | |
|             gps = [o for o in context.selected_objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|             for o in gps:
 | |
|                 if regex_num.match(o.name):
 | |
|                     o.name = o.name[4:]
 | |
|                     ct += 1
 | |
|                 self.report({'INFO'}, f'{ct}/{len(gps)} number prefix removed from object names')
 | |
| 
 | |
|             return {"FINISHED"}
 | |
| 
 | |
|         return context.window_manager.invoke_props_dialog(self)
 | |
| 
 | |
|     def draw(self, context):
 | |
|         layout = self.layout
 | |
|         layout.prop(self, 'all_objects')
 | |
|         if self.all_objects:
 | |
|             gp_ct = len([o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)])
 | |
|         else:
 | |
|             gp_ct = len([o for o in context.selected_objects if o.type == 'GPENCIL'])
 | |
| 
 | |
|         layout.prop(self, 'rename_data')
 | |
|         layout.label(text=f'{gp_ct} objects to renumber')
 | |
|         if not gp_ct:
 | |
|             layout.label(text='No Gpencil object to renumber', icon = 'ERROR')
 | |
| 
 | |
|     def execute(self, context):
 | |
|         if self.all_objects:
 | |
|             pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|         else:
 | |
|             pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
 | |
| 
 | |
|         def reversed_enumerate(collection: list):
 | |
|             for i in range(len(collection)-1, -1, -1):
 | |
|                 yield i, collection[i]
 | |
| 
 | |
|         fronts = []
 | |
| 
 | |
|         ## separate In Front objects:
 | |
| 
 | |
|         for i, o in reversed_enumerate(pool):
 | |
|             if o.show_in_front:
 | |
|                 fronts.append(pool.pop(i))
 | |
| 
 | |
|         cam_loc = context.scene.camera.matrix_world.to_translation()
 | |
| 
 | |
|         # filter by distance to camera object (considering origins)
 | |
|         pool.sort(key=lambda x: math.dist(x.matrix_world.to_translation(), cam_loc))
 | |
|         fronts.sort(key=lambda x: math.dist(x.matrix_world.to_translation(), cam_loc))
 | |
|         # re-insert fitlered infront object before others
 | |
|         pool = fronts + pool
 | |
| 
 | |
|         ct = 10
 | |
|         regex_num = re.compile(r'^(\d{3})_')
 | |
|         for o in pool:
 | |
|             renum = regex_num.search(o.name)
 | |
| 
 | |
|             if not renum:
 | |
|                 o.name = f'{str(ct).zfill(3)}_{o.name}'
 | |
| 
 | |
|             else:
 | |
|                 ## either replace or leave untouched
 | |
|                 # continue
 | |
|                 o.name =  f'{str(ct).zfill(3)}_{o.name[4:]}'
 | |
| 
 | |
|             ct += 10
 | |
|             if self.rename_data and o.name != o.data.name:
 | |
|                 o.data.name = o.name
 | |
| 
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| 
 | |
| class GPEXP_OT_check_masks(bpy.types.Operator):
 | |
|     bl_idname = "gp.check_masks"
 | |
|     bl_label = "Check Masks"
 | |
|     bl_description = "Check and report all masked GP layers"
 | |
|     bl_options = {"REGISTER", "UNDO"}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     def execute(self, context):
 | |
|         # if self.all_objects:
 | |
|         #     pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
 | |
|         # else:
 | |
|         #     pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
 | |
|         changes = []
 | |
|         pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
 | |
|         for o in pool:
 | |
|             for l in o.data.layers:
 | |
|                 if l.use_mask_layer:
 | |
|                     obj_stat = f'{o.name} >>'
 | |
|                     if not obj_stat in changes:
 | |
|                         changes.append(obj_stat)
 | |
|                         print(obj_stat)
 | |
| 
 | |
|                     hide_state = ' (hided)' if l.hide else ''
 | |
|                     text = f'  {l.info}{hide_state}:' # :masks:
 | |
|                     changes.append(text)
 | |
|                     print(text)
 | |
| 
 | |
|                     has_masks = False
 | |
|                     for ml in l.mask_layers:
 | |
|                         # 'hide', 'invert', 'name'
 | |
|                         h = ' hided' if ml.hide else ''
 | |
|                         i = ' (inverted)' if ml.invert else ''
 | |
|                         text = f'   - {ml.name}{h}{i}'
 | |
|                         changes.append(text)
 | |
|                         print(text)
 | |
|                         has_masks = True
 | |
| 
 | |
|                     if not has_masks:
 | |
|                         text = 'No masks!'
 | |
|                         changes.append(text)
 | |
|                         print(text)
 | |
|                     changes.append('')
 | |
| 
 | |
|         if changes:
 | |
|             fn.show_message_box(_message=changes, _title="Masks Check Report", _icon='INFO')
 | |
|         else:
 | |
|             fn.show_message_box(_message='No Masks!', _title="Masks Check Report", _icon='INFO')
 | |
| 
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
 | |
|     bl_idname = "gp.select_layer_in_comp"
 | |
|     bl_label = "Select Layer In Compositor"
 | |
|     bl_description = "Select associated render_layer node in compositing"
 | |
|     bl_options = {"REGISTER", "UNDO"}
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.object and context.object.type == 'GPENCIL'
 | |
| 
 | |
|     
 | |
|     def invoke(self, context, event):
 | |
|         self.additive = event.shift
 | |
|         return self.execute(context)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         gp = context.object.data
 | |
|         act = gp.layers.active
 | |
|         pool = fn.build_layers_targets_from_dopesheet(context)
 | |
|         if not pool:
 | |
|             self.report({'ERROR'}, 'No layers found in current GP dopesheet')
 | |
|             return {"CANCELLED"}
 | |
| 
 | |
|         if not context.scene.node_tree:
 | |
|             self.report({'ERROR'}, 'No compo node-tree in active scene')
 | |
|             return {"CANCELLED"}
 | |
| 
 | |
|         scn = context.scene
 | |
|         node_scene = fn.get_compo_scene(create=False) or scn
 | |
|         nodes = node_scene.node_tree.nodes
 | |
|         rl_nodes = [n for n in nodes if n.type == 'R_LAYERS']
 | |
|         if not rl_nodes:
 | |
|             self.report({'ERROR'}, 'No render layers nodes in active scene')
 | |
|             return {"CANCELLED"}
 | |
| 
 | |
|         # Deselect all nodes if shift is not pressed
 | |
|         if not self.additive:
 | |
|             for n in nodes:
 | |
|                 n.select = False
 | |
| 
 | |
|         used_vl = [n.layer for n in rl_nodes]
 | |
|         selected = []
 | |
|         infos = []
 | |
| 
 | |
|         for l in pool:
 | |
|             if not l.select:
 | |
|                 continue
 | |
|             vl_name = l.viewlayer_render
 | |
|             if not vl_name:
 | |
|                 mess = f'{l.info} has no viewlayers'
 | |
|                 print(mess)
 | |
|                 infos.append(mess)
 | |
|                 continue
 | |
| 
 | |
|             if not context.scene.view_layers.get(vl_name):
 | |
|                 mess = f'/!\ {l.info}: view layer "{vl_name}" does not exists '
 | |
|                 print(mess)
 | |
|                 infos.append(mess)
 | |
|                 continue
 | |
| 
 | |
|             if not vl_name in used_vl:
 | |
|                 mess = f'{l.info}: view layer "{vl_name}" not used in scene renderlayer nodes'
 | |
|                 print(mess)
 | |
|                 infos.append(mess)
 | |
|                 continue
 | |
| 
 | |
|             for n in rl_nodes:
 | |
|                 if n.layer == vl_name:
 | |
|                     print(f'{l.info} -> Select node {n.name}')
 | |
|                     selected.append(n.name)
 | |
|                     n.select = True
 | |
| 
 | |
|         if not infos and not selected:
 | |
|             self.report({'ERROR'}, 'Nothing selected')
 | |
|             return {"CANCELLED"}
 | |
| 
 | |
|         infos = infos + [f'-- Selected {len(selected)} nodes --']  + selected
 | |
|         fn.show_message_box(_message=infos, _title="Selected viewlayer in compo", _icon='INFO')
 | |
| 
 | |
|         # Change viewed scene if not in current scene
 | |
|         if selected and scn != node_scene:
 | |
|             context.window.scene = node_scene
 | |
|         return {"FINISHED"}
 | |
| 
 | |
| classes=(
 | |
| GPEXP_OT_auto_number_object,
 | |
| GPEXP_OT_lower_layers_name,
 | |
| GPEXP_OT_export_infos_for_compo,
 | |
| GPEXP_OT_layers_state,
 | |
| GPEXP_OT_check_masks,
 | |
| GPEXP_OT_select_layer_in_comp,
 | |
| )
 | |
| 
 | |
| def register():
 | |
|     for cls in classes:
 | |
|         bpy.utils.register_class(cls)
 | |
| 
 | |
| def unregister():
 | |
|     for cls in reversed(classes):
 | |
|         bpy.utils.unregister_class(cls) |