2021-09-17 16:31:26 +02:00
import bpy
from bpy . props import ( FloatProperty ,
BoolProperty ,
EnumProperty ,
StringProperty ,
IntProperty )
from . import fn
2022-01-22 19:13:11 +01:00
import math
import re
2022-01-24 23:14:12 +01:00
import json
from pathlib import Path
2021-09-17 16:31:26 +02:00
2022-01-24 13:15:59 +01:00
## TODO : export json info to re-setup layers in AE automagically
2022-01-24 23:14:12 +01:00
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 "
2022-01-26 16:32:33 +01:00
bl_options = { " REGISTER " }
2022-01-24 23:14:12 +01:00
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def invoke ( self , context , event ) :
self . l_infos = Path ( bpy . data . filepath ) . parent / ' render ' / ' infos.json '
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 ) :
dic = { }
pool = [ o for o in context . scene . objects if o . type == ' GPENCIL ' ]
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 = { }
2022-01-26 16:32:33 +01:00
## 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 :
2022-01-24 23:14:12 +01:00
mask = { }
if ml . hide :
continue
2022-01-26 16:32:33 +01:00
# mask['name'] = ml.name
2022-01-24 23:14:12 +01:00
if ml . invert : # create key get only if inverted
2022-01-26 16:32:33 +01:00
mask [ ' invert ' ] = ml . invert # ! no key if no invert
multi_mask [ ml . name ] = mask
2022-01-24 23:14:12 +01:00
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 :
2022-01-26 16:32:33 +01:00
self . l_infos . parent . mkdir ( exist_ok = True ) # create render folder if needed
2022-01-24 23:14:12 +01:00
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 " }
2021-09-17 16:31:26 +02:00
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 ' ,
2021-09-22 18:35:52 +02:00
default = True , description = ' On All object, else use selected objects ' ) # , options={'SKIP_SAVE'}
2021-09-17 16:31:26 +02:00
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'}
2021-09-22 18:35:52 +02:00
2021-10-11 16:47:22 +02:00
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 \n With a tolerance of one frame to avoid problem \n Affect all layers) ' ) # , options={'SKIP_SAVE'}
2021-09-22 18:35:52 +02:00
opacity_exclude_list : StringProperty ( name = ' Skip ' ,
default = ' MA ' , description = ' Skip prefixes from this list when changing opacity \n Separate multiple value with a comma (ex: MA,IN) ' ) # , options={'SKIP_SAVE'}
2021-09-17 16:31:26 +02:00
2021-10-11 16:47:22 +02:00
2021-09-17 16:31:26 +02:00
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def invoke ( self , context , event ) :
if event . alt :
self . all_objects = True
2022-01-26 16:32:33 +01:00
## 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 ' )
2021-09-17 16:31:26 +02:00
# return self.execute(context)
return context . window_manager . invoke_props_dialog ( self )
def draw ( self , context ) :
layout = self . layout
layout . prop ( self , ' all_objects ' )
2021-10-11 16:47:22 +02:00
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 ' )
2021-09-17 16:31:26 +02:00
layout . separator ( )
layout . label ( text = ' Set (or only perform a check): ' )
2021-09-22 18:35:52 +02:00
row = layout . row ( )
row . prop ( self , ' set_full_opacity ' )
if self . set_full_opacity :
row . prop ( self , ' opacity_exclude_list ' )
2021-09-17 16:31:26 +02:00
layout . prop ( self , ' set_use_lights ' )
layout . prop ( self , ' set_blend_mode ' )
# 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 ' ]
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
2021-10-11 16:47:22 +02:00
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 ' )
2021-09-17 16:31:26 +02:00
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}')
2022-01-24 13:15:59 +01:00
2021-09-17 16:31:26 +02:00
if l . opacity != 1 :
2021-09-22 18:35:52 +02:00
# 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
2021-09-17 16:31:26 +02:00
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 )
2022-01-24 11:43:47 +01:00
# changes.append(mess) # don't report disable use_light... too many messages
2021-09-17 16:31:26 +02:00
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
2022-01-24 13:15:59 +01:00
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
2021-10-11 16:47:22 +02:00
2021-09-17 16:31:26 +02:00
if used :
print ( )
if changes :
changes . append ( ' ' )
fn . show_message_box ( _message = changes , _title = " Layers Check Report " , _icon = ' INFO ' )
# render = bpy.data.scenes.get('Render')
# if not render:
# print('SKIP, no Render scene')
# return {"CANCELLED"}
return { " FINISHED " }
2021-09-21 18:23:25 +02:00
class GPEXP_OT_lower_layers_name ( bpy . types . Operator ) :
bl_idname = " gp.lower_layers_name "
2021-09-22 12:06:40 +02:00
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) "
2021-09-21 18:23:25 +02:00
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 = False , description = ' On All object, else use selected objects ' ) # , options={'SKIP_SAVE'}
2021-09-22 12:06:40 +02:00
object_name : BoolProperty ( name = ' Normalize Object Name ' ,
2021-09-21 18:23:25 +02:00
default = True , description = ' Make the object name lowercase ' ) # , options={'SKIP_SAVE'}
2021-09-22 12:06:40 +02:00
layer_name : BoolProperty ( name = ' Normalize Layers Names ' ,
2021-09-21 18:23:25 +02:00
default = True , description = ' Make the layers name lowercase ' ) # , options={'SKIP_SAVE'}
2021-09-22 12:06:40 +02:00
# dash_to_undescore : BoolProperty(name='Dash To Underscore',
# default=True, description='Make the layers name lowercase') # , options={'SKIP_SAVE'}
2021-09-21 18:23:25 +02:00
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 ' ] )
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 ' )
2021-09-22 12:06:40 +02:00
# if self.layer_name:
# box = layout.box()
# box.prop(self, 'dash_to_undescore')
2021-09-21 18:23:25 +02:00
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 ' ]
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
2021-09-22 12:06:40 +02:00
ob . name = ob . name . lower ( ) . replace ( ' - ' , ' _ ' )
2021-09-21 18:23:25 +02:00
if rename_data :
2021-09-22 12:06:40 +02:00
ob . data . name = ob . name
2021-09-21 18:23:25 +02:00
if self . layer_name :
for l in ob . data . layers :
2021-09-22 12:28:35 +02:00
# if self.dash_to_undescore:
l . info = l . info . replace ( ' - ' , ' _ ' )
2021-09-22 12:06:40 +02:00
fn . normalize_layer_name ( l ) # default : lower=True, dash_to_underscore=self.dash_to_undescore
2021-09-21 18:23:25 +02:00
return { " FINISHED " }
2022-01-22 19:13:11 +01:00
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 \n Ctrl + 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 = False , 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 ' ]
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 ' ] )
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 ' ]
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 " }
2022-01-24 13:15:59 +01:00
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 ' ]
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 " }
2021-09-17 16:31:26 +02:00
classes = (
2022-01-24 13:15:59 +01:00
GPEXP_OT_auto_number_object ,
2022-01-24 23:14:12 +01:00
GPEXP_OT_lower_layers_name ,
GPEXP_OT_export_infos_for_compo ,
GPEXP_OT_layers_state ,
2022-01-24 13:15:59 +01:00
GPEXP_OT_check_masks ,
2021-09-17 16:31:26 +02:00
)
def register ( ) :
for cls in classes :
bpy . utils . register_class ( cls )
def unregister ( ) :
for cls in reversed ( classes ) :
bpy . utils . unregister_class ( cls )