1.8.4 - fix: select node from layer - changed: Check for problems: object numbering check does not list as error if parent part has number prefix (previously checked only for last part)
688 lines
26 KiB
Python
688 lines
26 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_restore_layers_state(bpy.types.Operator):
|
|
bl_idname = "gp.restore_layers_state"
|
|
bl_label = "Restore Layers State "
|
|
bl_description = "Restore original state of layers"
|
|
bl_options = {"REGISTER"} # , "UNDO"
|
|
|
|
def execute(self, context):
|
|
layers_info_path = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
|
|
if not layers_info_path.exists():
|
|
self.report({"ERROR"}, 'No Info Found')
|
|
return {"CANCELLED"}
|
|
|
|
layers_info = json.loads(layers_info_path.read_text(encoding='utf-8'))
|
|
|
|
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
|
|
for gp in pool:
|
|
for layer_name, info in layers_info.items():
|
|
if gp.name == info['object']:
|
|
if not (layer := gp.data.layers.get(layer_name)):
|
|
return
|
|
|
|
if "opacity" in info:
|
|
layer.opacity = info['opacity']
|
|
|
|
if "blend_mode" in info:
|
|
layer.blend_mode = info['blend_mode']
|
|
|
|
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)
|
|
|
|
no_popup : BoolProperty(name='No Popup',
|
|
default=False, description='To use for call in CLI or from other operators', options={'SKIP_SAVE'})
|
|
|
|
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, MASK, mask, MSK, msk', 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
|
|
|
|
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')
|
|
|
|
if self.no_popup:
|
|
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:
|
|
if l.opacity == 0:
|
|
## Skip layer with zero opacity
|
|
print(f'Skipped layer opacity reset (0 opacity) : {l.info}')
|
|
|
|
elif any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x):
|
|
# Skip layer if name has exclusion prefix
|
|
print(f'Skipped layer opacity reset (prefix in exclusion list) : {l.info}')
|
|
else:
|
|
## Set full opacity
|
|
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')
|
|
|
|
if not self.no_popup:
|
|
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 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_restore_layers_state,
|
|
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) |