diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d3bc3..8e3a2b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog + + + +0.1.1 + +- ui: show number of selected obj +- ui: show panels in dopesheet and node windows + 0.1.0 first WIP usable version \ No newline at end of file diff --git a/OP_add_layer.py b/OP_add_layer.py index 4942572..f0a16c3 100644 --- a/OP_add_layer.py +++ b/OP_add_layer.py @@ -69,7 +69,7 @@ class GPEXP_OT_add_objects_to_render(bpy.types.Operator): if not scn: self.report({'ERROR'}, 'Could not found default scene') return {"CANCELLED"} - export_gp_objects([o for o in scn.objects if o.type == 'GPENCIL']) + export_gp_objects([o for o in scn.objects if o.type == 'GPENCIL' and not o.hide_get()]) return {"FINISHED"} diff --git a/OP_clear.py b/OP_clear.py index df68ae9..a51041e 100644 --- a/OP_clear.py +++ b/OP_clear.py @@ -11,7 +11,7 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator): def poll(cls, context): return True - mode : bpy.props.StringProperty(options={'SKIP_SAVE'}) + mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) def execute(self, context): @@ -19,13 +19,25 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator): if not render: print('SKIP, no Render scene') - # clear all nodes + # clear all nodes in frames if render.use_nodes: for i in range(len(render.node_tree.nodes))[::-1]: + + # skip frames to delete later + if render.node_tree.nodes[i].type == 'FRAME': + continue + + # skip unparented nodes if not render.node_tree.nodes[i].parent: continue render.node_tree.nodes.remove(render.node_tree.nodes[i]) + # delete all framesWorki + if render.use_nodes: + for i in range(len(render.node_tree.nodes))[::-1]: + if render.node_tree.nodes[i].type == 'FRAME': + render.node_tree.nodes.remove(render.node_tree.nodes[i]) + # clear all view_layers for vl in reversed(render.view_layers): if ' / ' in vl.name: @@ -37,6 +49,9 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator): ng.use_fake_user = False bpy.data.node_groups.remove(ng) + if self.mode == 'COMPLETE': + bpy.data.scenes.remove(render) + return {"FINISHED"} classes=( diff --git a/OP_manage_outputs.py b/OP_manage_outputs.py new file mode 100644 index 0000000..4030767 --- /dev/null +++ b/OP_manage_outputs.py @@ -0,0 +1,35 @@ +import bpy + + +class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator): + bl_idname = "gp.mute_toggle_output_nodes" + bl_label = "Mute Toggle output nodes" + bl_description = "Mute / Unmute all output nodes" + bl_options = {"REGISTER"} + + mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) + + def execute(self, context): + # scene = bpy.data.scenes.get('Render') + ct = 0 + for n in context.scene.node_tree.nodes: + if n.type != 'OUTPUT_FILE': + continue + n.mute = self.mute + ct += 1 + + state = 'muted' if self.mute else 'unmuted' + self.report({"INFO"}, f'{ct} nodes {state}') + return {"FINISHED"} + +classes=( +GPEXP_OT_mute_toggle_output_nodes, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/OP_merge_layers.py b/OP_merge_layers.py index 9dd304a..55878d6 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -1,7 +1,8 @@ import bpy +from . import fn +def merge_layers(rlayers, obname=None, active=None): -def merge_layers(rlayers, obname=None): print(f'Merging {len(rlayers)} layers') print('->', [r.layer for r in rlayers]) print() @@ -9,24 +10,59 @@ def merge_layers(rlayers, obname=None): if not rlayers: return ('ERROR', 'No render layer sent to merge') - ng = rlayers[0].outputs[0].links[0].to_node - rlayers.sort(key=lambda x: x.location.y, reverse=True) + # get node group + # ng = rlayers[0].outputs[0].links[0].to_node + + # sort RL descending + rlayers.sort(key=lambda n: fn.real_loc(n).y, reverse=True) + + vl_name = active.layer + if not vl_name: + vl_name = rlayers[0].layer # change colors of those nodes - color = random_color() + color = fn.random_color() for n in rlayers: n.use_custom_color = True n.color = color - - # get inside socket (group input) from outside socket list (should be already ordered) + obname, lname = vl_name.split(' / ') + lname = bpy.path.clean_name(lname) - ## by name - # for i, inp in enumerate(ng.node_tree.inputs): - # if inp.name == + ng_name = f'merge_NG_{obname}' # only object name - # by connection order + ## clear nodes groups duplication (.00?) + fn.clear_nodegroup(ng_name, full_clear=False) + + # get set nodegroup from vlayer name + if not ng: + ng = nodes.get(ng_name) + + if not ng: + ngroup = bpy.data.node_groups.get(ng_name) + # full clear True if exists but not used + if ngroup and ngroup.users == 0: + ngroup = None + fn.clear_nodegroup(ng_name, full_clear=True) + + if not ngroup: + # delete and recreate ? + print(f'create nodegroup {ng_name}') + ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree') + + ng = fn.create_node('CompositorNodeGroup', tree=scene.node_tree, location=(fn.real_loc(rlayer)[0] + 600, fn.real_loc(rlayer)[1]), width=400) # (rlayer.location[0] + 600, rlayer.location[1]) + if frame: + ng.parent= frame + ng.node_tree = ngroup + ng.name = ngroup.name + + ng_in = fn.create_node('NodeGroupInput', tree=ngroup, location=(-600,0)) + ng_out = fn.create_node('NodeGroupOutput', tree=ngroup, location=(600,0)) + + + + """ socket_list = [] grp_sockets = [] @@ -48,65 +84,95 @@ def merge_layers(rlayers, obname=None): if inp.name != grps.name: print(f'\n! Problem ! : {inp.name}, {grps.name}') return + """ ## # JUST CREATE ANOTHER GROUP NODE FOR THE MERGE ! ## +class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): + bl_idname = "gp.merge_selected_dopesheet_layers" + bl_label = "Merge selected layers view_layers " + bl_description = "Merge view layers of selected gp layers to a new dedicated file output" + bl_options = {"REGISTER"} -def merge_selected_layers(): - '''Merge command from selected GP layers''' - ob = bpy.context.object - layer_names = [l.info for l in ob.data.layers if l.select and not l.hide] - print("layer_names", layer_names)#Dbg - - if len(layer_names) < 2: - print(f'Should select multiple layers for merging') - return + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + def execute(self, context): + # merge_selected_layers() # function to merge from GP dopesheet + ob = bpy.context.object + layer_names = [l.info for l in ob.data.layers if l.select and not l.hide] + print("layer_names", layer_names)#Dbg + + if len(layer_names) < 2: + print(f'Should select multiple layers for merging') + return + + render = bpy.data.scenes.get('Render') + if render: + nodes = render.node_tree.nodes + + clean_ob_name = bpy.path.clean_name(ob.name) + rlayers = [] + for l in layer_names: + ## identifier is clean_name(ob.name).layer_name + + idname = f'{clean_ob_name}.{l}' + + # check the render layer that have a parent frame + rlayer = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == idname and n.parent] + if not rlayer: + # send to function to generate the rlayer and connect + # rlayer = creation + continue + + rlayers.append(rlayer) + + merge_layers(rlayers, obname=clean_ob_name) + + return {"FINISHED"} + + +class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): + bl_idname = "gp.merge_selected_viewlayer_nodes" + bl_label = "Merge selected view_layers " + bl_description = "Merge selected view layers to a new dedicated file output" + bl_options = {"REGISTER"} + + def execute(self, context): + render = bpy.data.scenes.get('Render') + if not render: + print('No render scene') + return - render = bpy.data.scenes.get('Render') - if render: nodes = render.node_tree.nodes - - clean_ob_name = bpy.path.clean_name(ob.name) - rlayers = [] - for l in layer_names: - ## identifier is clean_name(ob.name).layer_name - - idname = f'{clean_ob_name}.{l}' - - # check the render layer that have a parent frame - rlayer = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == idname and n.parent] - if not rlayer: - # send to function to generate the rlayer and connect - # rlayer = creation - continue + selection = [n for n in nodes if n.select and n.type == 'R_LAYERS'] - rlayers.append(rlayer) + if not nodes.active in selection: + self.report({'ERROR'}, 'The active node not within the render layer selection (used to define out name)') + return {'CANCELLED'} - merge_layers(rlayers, obname=clean_ob_name) + # should be from the same object: + if not all(selection[0].layer.split('.')[0] == n.layer.split('.')[0] for n in selection): + print('Merge -> Not every nodes start with the same object') + + # obname = selection[0].layer.split('.')[0] + merge_layers(selection, nodes.active) + + return {"FINISHED"} -def merge_selected_render_layers(): - '''Merge command from selected render layers nodes''' - render = bpy.data.scenes.get('Render') - if not render: - print('No render scene') - return - - nodes = render.node_tree.nodes - selection = [n for n in nodes if n.select and n.type == 'R_LAYERS'] - - # should be from the same object: - assert all(selection[0].layer.split('.')[0] == n.layer.split('.')[0] for n in selection), 'Not every nodes start with the same object' +classes=( +GPEXP_OT_merge_selected_dopesheet_layers, +GPEXP_OT_merge_selected_viewlayer_nodes, +) - # obname = selection[0].layer.split('.')[0] - merge_layers(selection) - - - - -# merge_selected_layers() # function to merge from GP dopesheet - -merge_selected_render_layers() # function to merge from nodegroup +def register(): + for cls in classes: + bpy.utils.register_class(cls) +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/__init__.py b/__init__.py index d2776b3..c85f065 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "GP exporter", "description": "Organise export of gp layers through compositor output", "author": "Samuel Bernou", - "version": (0, 1, 0), + "version": (0, 1, 1), "blender": (2, 93, 0), "location": "View3D", "warning": "", @@ -11,7 +11,9 @@ bl_info = { from . import OP_add_layer +from . import OP_merge_layers from . import OP_clear +from . import OP_manage_outputs from . import ui import bpy @@ -22,6 +24,8 @@ def register(): OP_add_layer.register() OP_clear.register() + OP_merge_layers.register() + OP_manage_outputs.register() ui.register() # bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = PROJ_PGT_settings) @@ -30,6 +34,8 @@ def unregister(): return ui.unregister() + OP_manage_outputs.unregister() + OP_merge_layers.unregister() OP_clear.unregister() OP_add_layer.unregister() diff --git a/fn.py b/fn.py index 0f1ed92..de75b33 100644 --- a/fn.py +++ b/fn.py @@ -35,13 +35,21 @@ def set_settings(scene=None): scene = bpy.context.scene scene.eevee.taa_render_samples = 1 scene.grease_pencil_settings.antialias_threshold = 0 + # add transparent toggle on ? def get_render_scene(): '''Get / Create a scene named Render''' render = bpy.data.scenes.get('Render') if not render: render = bpy.data.scenes.new('Render') - set_settings(render) # setup specific settings + + ## link cameras (and lights ?) + for ob in bpy.context.scene.objects: + if ob.type in ('CAMERA', 'LIGHT'): + render.collection.objects.link(ob) + + # set adapted render settings (no AA) + set_settings(render) render.use_nodes = True return render @@ -252,4 +260,10 @@ def connect_to_group_input(n): val = connect_to_group_input(i.links[0].from_node) if val: return val - return False \ No newline at end of file + return False + +def random_color(alpha=False): + import random + if alpha: + return (random.uniform(0,1), random.uniform(0,1), random.uniform(0,1), 1) + return (random.uniform(0,1), random.uniform(0,1), random.uniform(0,1)) \ No newline at end of file diff --git a/gen_vlayer.py b/gen_vlayer.py index 64edd04..987a5d9 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -229,7 +229,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): # get frame object and their contents # dict like : {objname : [layer_nodeA, layer_nodeB,...]} - frame_dic = {f.label: [n for n in nodes if n.type == 'R_LAYERS' and n.parent and n.parent.name == f.name and n.layer != 'View Layer'] + frame_dic = {f.label: [n for n in nodes if n.type == 'R_LAYERS' and n.parent and n.parent.name == f.name and '/' in n.layer] # n.layer != 'View Layer' for f in nodes if f.type == 'FRAME'} # debug print diff --git a/ui.py b/ui.py index d97ce32..1c0e330 100644 --- a/ui.py +++ b/ui.py @@ -3,38 +3,82 @@ from bpy.types import Panel # from .preferences import get_addon_prefs -# 3D view panel +# Node view panel class GPEXP_PT_gp_node_ui(Panel): bl_space_type = "NODE_EDITOR" # "VIEW_3D" bl_region_type = "UI" - bl_category = "Item" + bl_category = "View" bl_label = "Gpencil Render Manager" def draw(self, context): layout = self.layout + + row = layout.row() + ct = len([n for n in context.scene.node_tree.nodes if n.type == 'R_LAYERS' and n.select]) + txt = f'Merge {ct} Layer Nodes' + row.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text=txt) + row.enabled = ct > 1 + + layout.separator() + + row=layout.row() + row.operator('gp.mute_toggle_output_nodes', icon='NODE_INSERT_ON', text='Mute').mute = True + row.operator('gp.mute_toggle_output_nodes', icon='NODE_INSERT_OFF', text='Unmute').mute = False + + layout.separator() + + col=layout.column() + col.operator('gp.clear_render_tree', icon='X', text='Clear Render Tree') + col.operator('gp.clear_render_tree', icon='X', text='Clear Delete Render Scene').mode = "COMPLETE" + - layout.operator('gp.clear_render_tree', icon='X', text='Full Clear Render Tree') # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'ALL' # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'SELECTED' - + # layout.operator('gp.merge_layers', icon='X', text='Merge selected nodes') +class GPEXP_PT_gp_dopesheet_ui(Panel): + bl_space_type = 'DOPESHEET_EDITOR' + bl_region_type = 'UI' + # bl_category = "Item" + bl_parent_id='DOPESHEET_PT_gpencil_mode' + bl_label = "Gpencil Render Manager" + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + def draw(self, context): + layout = self.layout + layout.label(text=context.object.name) + + ct = len([l for l in context.object.data.layers if l.select]) + txt = f'Merge {ct} layers' + + # merge layers from dopesheet + layout.operator('gp.merge_selected_dopesheet_layers', text=txt, ) + layout.enabled= ct > 1 def manager_ui(self, context): '''appended to DATA_PT_gpencil_layers''' layout = self.layout - # on layers + + ## On layers if context.object and context.object.type == 'GPENCIL': txt = f'{len([l for l in context.object.data.layers if l.select])} Layer(s) To Render' else: txt = 'Layer To Render' layout.operator('gp.add_layer_to_render', icon='RENDERLAYERS', text=txt) - # on objects - layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Selected Object To Render').mode='SELECTED' + ## On objects + # txt = 'Selected Object To Render' + if context.scene.name != 'Render': + txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' + layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED' layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All GP at once').mode='ALL' + # ## function to append in a menu # def palette_manager_menu(self, context): # """Palette menu to append in existing menu""" @@ -50,6 +94,7 @@ def manager_ui(self, context): classes=( GPEXP_PT_gp_node_ui, +GPEXP_PT_gp_dopesheet_ui, ) def register():