diff --git a/CHANGELOG.md b/CHANGELOG.md index b2888c4..efaeed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Activate / deactivate layer opacity according to prefix Activate / deactivate all masks using MA layers --> +1.1.1 + +- changed: autobuild beta + 1.1.0 - added: `autobuild` button (partial auto-buildfor now) diff --git a/OP_add_layer.py b/OP_add_layer.py index 28db903..716487b 100644 --- a/OP_add_layer.py +++ b/OP_add_layer.py @@ -43,23 +43,6 @@ class GPEXP_OT_add_layer_to_render(bpy.types.Operator): return {"FINISHED"} - -def export_gp_objects(oblist, exclude_list=[], scene=None): - # Skip layer containing element in excluyde list - if not isinstance(oblist, list): - oblist = [oblist] - - for ob in oblist: - for l in ob.data.layers: - # if l.hide: - # continue - if l.hide or any(x + '_' in l.info for x in exclude_list): # exclude hided ? - l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude" - continue - - _vl, _cp = gen_vlayer.get_set_viewlayer_from_gp(ob, l, scene=scene) # scene=fn.get_render_scene()) - - ## send operator with mode ALL or SELECTED to batch build class GPEXP_OT_add_objects_to_render(bpy.types.Operator): bl_idname = "gp.add_object_to_render" @@ -84,14 +67,14 @@ class GPEXP_OT_add_objects_to_render(bpy.types.Operator): excludes = [] # ['MA', 'IN'] # Get list dynamically if self.mode == 'SELECTED': - export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn) + gen_vlayer.export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn) elif self.mode == 'ALL': # scn = bpy.data.scenes.get('Scene') # if not scn: # self.report({'ERROR'}, 'Could not found default scene') # return {"CANCELLED"} - export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn) + gen_vlayer.export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn) return {"FINISHED"} diff --git a/OP_auto_build.py b/OP_auto_build.py index f5da6cd..8c62dad 100644 --- a/OP_auto_build.py +++ b/OP_auto_build.py @@ -2,6 +2,48 @@ import bpy from pathlib import Path from . import gen_vlayer, fn + +def batch_setup_render_scene(context=None): + '''A series of setup actions for Render scene: + - renumber fileout + - Clean compo Tree + - Go to camera view in visible viewports + - Swap to bg cam + ''' + + if context is None: + context = bpy.context + + render_scn = context.scene + + ## Renumber File outputs + print('Renumber File outputs') + for fo in render_scn.node_tree.nodes: + if fo.type == 'OUTPUT_FILE': + fn.renumber_keep_existing(fo) + + ## Swap to bg_cam (if any) + if render_scn.objects.get('bg_cam') and (not render_scn.camera or render_scn.camera.name != 'bg_cam'): + print('Swap to bg cam') + bpy.ops.gp.swap_render_cams() + + ## Go to camera view in visible viewports + print('Go to camera view in visible viewports') + if render_scn.camera: + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + area.spaces.active.region_3d.view_perspective = 'CAMERA' + + ## Clean compo Tree + print('Clean compo Tree') + bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', use_render_scene=True) + # bpy.ops.gp.clean_compo_tree('INVOKE_DEFAULT', use_render_scene=True) + + ## Trigger check file before finishing ? + # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') + class GPEXP_OT_render_auto_build(bpy.types.Operator): bl_idname = "gp_export.render_auto_build" bl_label = "Auto-Build" @@ -12,58 +54,51 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type == 'GPENCIL' + excluded_prefix : bpy.props.StringProperty( + name='Excluded Layer By Prefix', default='GP,RG,PO', + description='Exclude layer to send to render by prefix (comma separated list)') + + ## TODO: add props to fine tune the auto-build # mode : bpy.props.StringProperty(options={'SKIP_SAVE'}) def execute(self, context): - ''' - ob = context.object - layer = ob.data.layers.active - if not layer: - self.report({'ERROR'}, 'No active layer') - return {"CANCELLED"} - - ct = 0 - # send scene ? - hided = 0 - for l in ob.data.layers: - if not l.select: - if not l.viewlayer_render: - l.viewlayer_render = fn.get_view_layer('exclude').name - continue - gen_vlayer.get_set_viewlayer_from_gp(ob, l) - - if l.hide: - hided += 1 - ct += 1 - - if hided: - self.report({'WARNING'}, f'{hided}/{ct} layers are hided !') - - else: - self.report({'INFO'}, f'{ct} layer(s) added to scene "Render"') - ''' - + print('-- Auto-build Render scene --\n') ## TODO: add colors to layers (specified in ENV or hardcoded for now...) - ## Option: Maybe find a way to create a color from prefix hash ? (wlways give unique color with same prefix on other project!) + ## Option: Maybe find a way to create a color from prefix hash ? (always give unique color with same prefix on other project!) render_scn = bpy.data.scenes.get('Render') if render_scn: self.report({'ERROR'}, 'A "Render" scene already exists') return {'CANCELLED'} + ob_list = [o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)] + if not ob_list: + self.report({'ERROR'}, 'No GP object to render found') + return {'CANCELLED'} + print('GP objects to send:') + for o in ob_list: + print(f' - {o.name}') ## Trigger rename lowercase - bpy.ops.gp.lower_layers_name() + print('Trigger rename lowercase') + bpy.ops.gp.lower_layers_name('EXEC_DEFAULT') + # bpy.ops.gp.lower_layers_name('INVOKE_DEFAULT') ## Trigger renumber by distance - bpy.ops.gp.auto_number_object() + print('Trigger renumber by distance') + bpy.ops.gp.auto_number_object('EXEC_DEFAULT') + # bpy.ops.gp.auto_number_object('INVOKE_DEFAULT') ## Export layer infos ? (skip if json already exists) + print('Export layer infos (skip if json already exists)') bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT', skip_check=True) ## Send all GP to render scene - bpy.ops.gp.add_object_to_render(mode="ALL") + print('Send all GP to render scene') + # bpy.ops.gp.add_object_to_render(mode="ALL") # Ops to send all + gen_vlayer.export_gp_objects(ob_list, exclude_list=self.excluded_prefix) # Create render scene OTF ## Switch to new Render Scene + print('Switch to new Render Scene') render_scn = bpy.data.scenes.get('Render') if not render_scn: self.report({'ERROR'}, 'No render scene found') @@ -72,11 +107,11 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): ## Change to GP workspace (if needed) if context.window.workspace.name != 'GP Render': + print('Change to GP workspace') if (render_wkspace := bpy.data.workspaces.get('GP Render')): context.window.workspace = render_wkspace else: render_wkspace_filepath = Path(bpy.utils.user_resource('SCRIPTS'), 'startup', 'bl_app_templates_user', 'GP', 'startup.blend') - print('render workspace', render_wkspace_filepath.exists()) ret = bpy.ops.workspace.append_activate(idname='GP Render', filepath=str(render_wkspace_filepath)) print('ret: ', ret) if ret != {'FINISHED'}: @@ -87,20 +122,61 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): print('No GP render workspace available') ## Group all adjacent layer type - + print('Group all adjacent layer type') + for ob in ob_list: + fn.group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=['GP', 'PO', 'RG'], first_name=True) + # bpy.ops.gp_export.render_scene_setup() # next render scene setup at once + ## Renumber File outputs + print('Renumber File outputs') + for fo in render_scn.node_tree.nodes: + if fo.type == 'OUTPUT_FILE': + fn.renumber_keep_existing(fo) + + ## Swap to bg_cam (if any) + if render_scn.objects.get('bg_cam') and (not render_scn.camera or render_scn.camera.name != 'bg_cam'): + print('Swap to bg cam') + bpy.ops.gp.swap_render_cams() + + ## Go to camera view in visible viewports + if render_scn.camera: + print('Go to camera view in visible viewports') + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + area.spaces.active.region_3d.view_perspective = 'CAMERA' + + ## Clean compo Tree + print('Clean compo Tree') + # bpy.ops.gp.clean_compo_tree('INVOKE_DEFAULT', use_render_scene=True) + bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', use_render_scene=True) ## Trigger check file before finishing ? - - - ## note: After all these operation, a ctrl+Z might crash - + # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') + ## Note: After all these operation, a ctrl+Z might crash + print('\nDone.') return {"FINISHED"} +class GPEXP_OT_render_scene_setup(bpy.types.Operator): + bl_idname = "gp_export.render_scene_setup" + bl_label = "Batch Setup Render Scene" + bl_description = "Batch some actions to setup render scene:\ + \n- renumber file output nodes\ + \n- Clean compo Tree\ + \n- Go to camera view in visible viewports\ + \n- Swap to bg cam" + bl_options = {"REGISTER"} + + def execute(self, context): + print('-- Auto-setup Render scene --\n') + batch_setup_render_scene(context=context) + return {"FINISHED"} classes=( GPEXP_OT_render_auto_build, +GPEXP_OT_render_scene_setup, ) def register(): diff --git a/OP_clean.py b/OP_clean.py index eb3cd41..d2e2228 100644 --- a/OP_clean.py +++ b/OP_clean.py @@ -50,13 +50,17 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator): return {"FINISHED"} """ - class GPEXP_OT_clean_compo_tree(bpy.types.Operator): bl_idname = "gp.clean_compo_tree" bl_label = "Clean Compo Tree" bl_description = "Pop up menu with cleaning options" bl_options = {"REGISTER", "UNDO"} + # Internal prop (use when launching from python) + use_render_scene : bpy.props.BoolProperty(name="Use Render Scene", + description="Force the clean on scene named Render, abort if not exists (not exposed)", + default=False, options={'SKIP_SAVE'}) + clear_unused_view_layers : bpy.props.BoolProperty(name="Clear unused view layers", description="Delete view layer that aren't used in the nodetree anymore", default=True) @@ -86,7 +90,6 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator): return True def invoke(self, context, event): - # self.nodes = context.object return context.window_manager.invoke_props_dialog(self) def draw(self, context): @@ -107,12 +110,14 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator): # box.prop(self, 'reorder_inputs') # box.prop(self, 'fo_clear_disconnected') - def execute(self, context): - # render = bpy.data.scenes.get('Render') - # if not render: - # print('SKIP, no Render scene') - # return {"CANCELLED"} - render = context.scene + def execute(self, context): + if self.use_render_scene: + render = bpy.data.scenes.get('Render') + if not render: + print('SKIP clean_compo_tree, No "Render" scene !') + return {"CANCELLED"} + else: + render = context.scene nodes = render.node_tree.nodes if self.clear_unused_view_layers: diff --git a/OP_merge_layers.py b/OP_merge_layers.py index fe44d5d..ae3bb9a 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -114,73 +114,6 @@ def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None) return ng, out -def merge_gplayer_viewlayers(ob, act=None, layers=None): - if act is None: - act = ob.data.layers.active - if layers is None: - layers = [l for l in ob.data.layers if l.select and l != act] - - rd_scn = bpy.data.scenes.get('Render') - if not rd_scn: - return ({'ERROR'}, 'Viewlayers needs to be generated first!') - - if not act.viewlayer_render: - return ({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') - - # list layers and viewlayers - vls = [rd_scn.view_layers.get(l.viewlayer_render) for l in layers - if l.viewlayer_render and l.viewlayer_render != act.viewlayer_render and rd_scn.view_layers.get(l.viewlayer_render)] - - vl_names = [v.name for v in vls] - - for n in reversed(rd_scn.node_tree.nodes): - if n.type == 'R_LAYERS' and n.layer in vl_names: - for lnk in n.outputs[0].links: - grp = lnk.to_node - if grp.type != 'GROUP': - continue - if not grp.name.startswith('NG'): - continue - sockin = lnk.to_socket - sockout = grp.outputs.get(sockin.name) - if not sockout: - continue - - for grplink in sockout.links: - if grplink.to_node.type != 'OUTPUT_FILE': - continue - fo_socket = grplink.to_socket - fo = grplink.to_node - fo.file_slots.remove(fo_socket) - - # remove input and output from group - # grp.inputs.remove(sockin) # do not clear inside !! - # grp.outputs.remove(sockout) # do not clear inside !! - ngroup = grp.node_tree - for i in range(len(grp.inputs))[::-1]: - if grp.inputs[i].name == sockin.name: - ngroup.inputs.remove(ngroup.inputs[i]) - break - for i in range(len(grp.outputs))[::-1]: - if grp.outputs[i].name == sockout.name: - ngroup.outputs.remove(ngroup.outputs[i]) - break - - # remove render_layer node - rd_scn.node_tree.nodes.remove(n) - - # assign view layer from active to selected - for l in layers: - l.viewlayer_render = act.viewlayer_render - - ## delete unused_vl - - # used_vl_name = [n.layer for n in rd_scn.node_tree.nodes if n.type == 'R_LAYERS' and n.layer] - for vl in vls: - rd_scn.view_layers.remove(vl) - # if not vl.name in used_vl_name: - # rd_scn.view_layers.remove(vl) - class GPEXP_OT_merge_viewlayers_to_active(bpy.types.Operator): bl_idname = "gp.merge_viewlayers_to_active" bl_label = "Merge selected layers view_layers" @@ -207,11 +140,47 @@ class GPEXP_OT_merge_viewlayers_to_active(bpy.types.Operator): # self.report({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') # return {'CANCELLED'} - ret = merge_gplayer_viewlayers(ob, act=act, layers=layers) + ret = fn.merge_gplayer_viewlayers(ob, act=act, layers=layers) if isinstance(ret, tuple): self.report(*ret) return {"FINISHED"} + +class GPEXP_OT_auto_merge_adjacent_prefix(bpy.types.Operator): + bl_idname = "gpexp.auto_merge_adjacent_prefix" + bl_label = "Auto Merge Adjacent Prefix" + bl_description = "Automatically merge viewlayer and renderlayer of grouped layer prefix" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + excluded_prefix : bpy.props.StringProperty( + name='Excluded Prefix', default='GP,RG,PO', + description='Exclude comma separated prefix from merging viewlayer') + + first_name : bpy.props.BoolProperty(name='Merge On Bottom Layer', + default=True, + description='Keep the viewlayer of the bottom layer in groups, else upper layer') + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + return self.execute(context) + + def draw(self, context): + layout = self.layout + layout.label(text='Settings for auto-merge:') + layout.prop(self, 'excluded_prefix') + layout.prop(self, 'first_name') + + def execute(self, context): + prefix_list = [p.strip() for p in self.excluded_prefix.split(',')] + for ob in [o for o in context.selected_objects if o.type == 'GPENCIL']: + fn.group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=prefix_list, first_name=self.first_name) + return {"FINISHED"} + +# unused class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): bl_idname = "gp.merge_selected_dopesheet_layers" bl_label = "Merge selected layers nodes" @@ -313,6 +282,7 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): classes=( GPEXP_OT_merge_viewlayers_to_active, +GPEXP_OT_auto_merge_adjacent_prefix, GPEXP_OT_merge_selected_dopesheet_layers,# unused GPEXP_OT_merge_selected_viewlayer_nodes, ) diff --git a/fn.py b/fn.py index 3319a8f..2f88ab5 100644 --- a/fn.py +++ b/fn.py @@ -349,6 +349,103 @@ def get_frames_bbox(node_tree): ## -- nodes helper functions +def merge_gplayer_viewlayers(ob, act=None, layers=None): + if act is None: + act = ob.data.layers.active + if layers is None: + layers = [l for l in ob.data.layers if l.select and l != act] + + rd_scn = bpy.data.scenes.get('Render') + if not rd_scn: + return ({'ERROR'}, 'Viewlayers needs to be generated first!') + + if not act.viewlayer_render: + return ({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') + + # list layers and viewlayers + vls = [rd_scn.view_layers.get(l.viewlayer_render) for l in layers + if l.viewlayer_render and l.viewlayer_render != act.viewlayer_render and rd_scn.view_layers.get(l.viewlayer_render)] + + vl_names = [v.name for v in vls] + + for n in reversed(rd_scn.node_tree.nodes): + if n.type == 'R_LAYERS' and n.layer in vl_names: + for lnk in n.outputs[0].links: + grp = lnk.to_node + if grp.type != 'GROUP': + continue + if not grp.name.startswith('NG'): + continue + sockin = lnk.to_socket + sockout = grp.outputs.get(sockin.name) + if not sockout: + continue + + for grplink in sockout.links: + if grplink.to_node.type != 'OUTPUT_FILE': + continue + fo_socket = grplink.to_socket + fo = grplink.to_node + fo.file_slots.remove(fo_socket) + + # remove input and output from group + # grp.inputs.remove(sockin) # do not clear inside !! + # grp.outputs.remove(sockout) # do not clear inside !! + ngroup = grp.node_tree + for i in range(len(grp.inputs))[::-1]: + if grp.inputs[i].name == sockin.name: + ngroup.inputs.remove(ngroup.inputs[i]) + break + for i in range(len(grp.outputs))[::-1]: + if grp.outputs[i].name == sockout.name: + ngroup.outputs.remove(ngroup.outputs[i]) + break + + # remove render_layer node + rd_scn.node_tree.nodes.remove(n) + + # assign view layer from active to selected + for l in layers: + l.viewlayer_render = act.viewlayer_render + + ## delete unused_vl + + # used_vl_name = [n.layer for n in rd_scn.node_tree.nodes if n.type == 'R_LAYERS' and n.layer] + for vl in vls: + rd_scn.view_layers.remove(vl) + # if not vl.name in used_vl_name: + # rd_scn.view_layers.remove(vl) + +def group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=[], first_name=True): + '''Set viewlayer and renderlayers by Gp layer adjacent prefix + Call merge_gplayer_viewlayers with grouped prefix + :excluded_prefix: List of prefixes to exclude from merge or str with comma separated values + :first_name: Keep the viewlayer of the bottom layer in group, else last + ''' + + from itertools import groupby + re_prefix = re.compile(r'^([A-Z]{2})_') + + if isinstance(excluded_prefix, str): + excluded_prefix = [p.strip() for p in excluded_prefix.split(',')] + + ## Create adjacent grp list: [('CO', [layer1, layer2]), ('LN', [layer3, layer4])] + adjacent_prefix_groups = [ + (g[0], list(g[1])) for g in + groupby([l for l in ob.data.layers], + key=lambda l: re_prefix.search(l.info).group(1) if re_prefix.search(l.info) else '') + ] + + for prefix, layer_grp in adjacent_prefix_groups: + if len(layer_grp) < 2: + continue + if not prefix or prefix in excluded_prefix: + continue + + ref = layer_grp[0] if first_name else layer_grp[-1] + merge_gplayer_viewlayers(ob, act=ref, layers=layer_grp) + + def clear_nodegroup(name, full_clear=False): '''remove duplication of a nodegroup (.???) also remove the base one if full_clear True diff --git a/gen_vlayer.py b/gen_vlayer.py index 12e20a1..b004acc 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -222,6 +222,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): else: scene = fn.get_render_scene() + print('Set viewlayer Scene: ', scene.name) node_tree = scene.node_tree nodes = node_tree.nodes @@ -371,3 +372,20 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): fn.rearrange_frames(node_tree) return vl, cp + +def export_gp_objects(oblist, exclude_list=[], scene=None): + # Skip layer containing element in exclude list + if not isinstance(oblist, list): + oblist = [oblist] + if isinstance(exclude_list, str): + exclude_list = [p.strip() for p in exclude_list.split(',')] + + for ob in oblist: + for l in ob.data.layers: + # if l.hide: + # continue + if l.hide or any(x + '_' in l.info for x in exclude_list): # exclude hided ? + l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude" + continue + + _vl, _cp = get_set_viewlayer_from_gp(ob, l, scene=scene) # scene=fn.get_render_scene()) \ No newline at end of file diff --git a/ui.py b/ui.py index 4f46c66..8d8818d 100644 --- a/ui.py +++ b/ui.py @@ -204,6 +204,8 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): # merge layers from dopesheet row.operator('gp.merge_viewlayers_to_active', text=txt, icon='SELECT_EXTEND') row.enabled= ct > 1 + + col.operator('gpexp.auto_merge_adjacent_prefix', icon='SELECT_EXTEND') ## all and objects layout.separator()