diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3a2b4..5517d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ OR always duplicate (safe but heavy scenes...) if duplicate, need to "connect" with namespace ('_duprender') or something --> +0.2.0 + +- feat: merge selected viewlayer +- feat: mute/unmute all output nodes +- feat: cleaning options +- feat: renumbering / denumbering + 0.1.1 - ui: show number of selected obj diff --git a/OP_clean.py b/OP_clean.py new file mode 100644 index 0000000..3818790 --- /dev/null +++ b/OP_clean.py @@ -0,0 +1,157 @@ +import bpy +from . import fn + +## direct use (Pop up menu version below) +""" +class GPEXP_OT_clean_compo_tree(bpy.types.Operator): + bl_idname = "gp.clean_compo_tree" + bl_label = "Clean Compo Tree" + bl_description = "Reorder inputs/outputs and clear unused viewlayers" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + # mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) + + def execute(self, context): + render = bpy.data.scenes.get('Render') + if not render: + print('SKIP, no Render scene') + return {"CANCELLED"} + + print('re-arranging frames') + fn.rearrange_frames(render.node_tree) + + for n in render.node_tree.nodes: + if n.name.startswith('NG_'): + fn.reorder_inputs(n) + fn.reorder_outputs(n) + + # get output node to reorder output + out = None + for s in n.outputs: + if not s.is_linked: + continue + out = s.links[0].to_node + if out.type == 'OUTPUT_FILE': + break + if out: + fn.reorder_fileout(out, ng=n) + + + ## clear disconnected fileout ??... + # for fo in render.node_tree.nodes: + # if fo.type != 'OUTPUT_FILE': + # continue + # fn.clear_disconnected(fo) + + 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"} + + 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) + + arrange_frames : bpy.props.BoolProperty(name="Arrange Frames", + description="Re-arrange all frames Y positions" , + default=True) + + reorder_inputs : bpy.props.BoolProperty(name="Reorder I/O Sockets", + description="Reorder inputs/outputs of all 'NG_' nodegroup and their connected file output", + default=True) + + fo_clear_disconnected : bpy.props.BoolProperty(name="Remove Disconnected Export Inputs", + description="Clear any disconnected intput of every 'file output' node", + default=False) + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + # self.nodes = context.object + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + layout.prop(self, 'clear_unused_view_layers') + layout.prop(self, 'arrange_frames') + layout.prop(self, 'reorder_inputs') + + layout.separator() + layout.prop(self, 'fo_clear_disconnected') + if self.fo_clear_disconnected: + layout.label(text='Disconnected inputs are not exported', icon='INFO') + + # box = layout.box() + # box.prop(self, 'arrange_frames') + # 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"} + + nodes = render.node_tree.nodes + if self.clear_unused_view_layers: + used_rlayer_names = [n.layer for n in nodes if n.type == 'R_LAYERS'] + for rl in reversed(render.view_layers): + if rl.name in used_rlayer_names or rl.name == 'View Layer': + continue + render.view_layers.remove(rl) + + if self.arrange_frames: + print('re-arranging frames') + fn.rearrange_frames(render.node_tree) + + if self.reorder_inputs: + for n in nodes: + if n.name.startswith('NG_'): + fn.reorder_inputs(n) + fn.reorder_outputs(n) + + # get output node to reorder output + out = None + for s in n.outputs: + if not s.is_linked: + continue + out = s.links[0].to_node + if out.type == 'OUTPUT_FILE': + break + if out: + fn.reorder_fileout(out, ng=n) + + if self.fo_clear_disconnected: + for fo in nodes: + if fo.type != 'OUTPUT_FILE': + continue + fn.clear_disconnected(fo) + + + + return {"FINISHED"} + + + +classes=( +GPEXP_OT_clean_compo_tree, +) + +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_clear.py b/OP_clear.py index a51041e..251fe2c 100644 --- a/OP_clear.py +++ b/OP_clear.py @@ -18,6 +18,7 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator): render = bpy.data.scenes.get('Render') if not render: print('SKIP, no Render scene') + return {"CANCELLED"} # clear all nodes in frames if render.use_nodes: diff --git a/OP_connect_toggle.py b/OP_connect_toggle.py new file mode 100644 index 0000000..8874fd0 --- /dev/null +++ b/OP_connect_toggle.py @@ -0,0 +1,69 @@ +import bpy +from . import fn + + +class GPEXP_OT_reconnect_render_layer(bpy.types.Operator): + bl_idname = "gp.reconnect_render_layer" + bl_label = "Reconnect Render Layer" + bl_description = "Reconnect selected render layers" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + # mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) + + def execute(self, context): + node_tree = context.scene.node_tree + nodes = node_tree.nodes + + changed = [] + for n in nodes: + if not n.select or not n.type == 'R_LAYERS': + continue + + if not ' / ' in n.layer: + continue + + if n.outputs[0].is_linked: # already connected + continue + + # get namme + obname = n.layer.split()[0] + grp_name = f'NG_{obname}' + + + # get nodegroup + grp = nodes.get(grp_name) + if not grp: + print(f'{n.name} Node group not found : {n.layer} !-> {grp_name}') + continue + + inp = grp.inputs.get(n.layer) + if not inp: + print(f'{n.name} no inputs name "{n.layer}" in group {grp_name}') + continue + + # reconnect + node_tree.links.new(n.outputs[0], inp) + changed.append(f'{n.name} ({n.layer}) to {grp_name}') + + if changed: + self.report({'INFO'}, f'{len(changed)} nodes reconnected') + else: + self.report({'WARNING'}, f'Could not reconnect, see console') + + return {"FINISHED"} + +classes=( +GPEXP_OT_reconnect_render_layer, +) + +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 55878d6..5964a8b 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -1,7 +1,8 @@ import bpy +import re from . import fn -def merge_layers(rlayers, obname=None, active=None): +def merge_layers(rlayers, obname=None, active=None, disconnect=True): print(f'Merging {len(rlayers)} layers') print('->', [r.layer for r in rlayers]) @@ -16,79 +17,93 @@ def merge_layers(rlayers, obname=None, active=None): # 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 + node_tree = rlayers[0].id_data + nodes = node_tree.nodes + links = node_tree.links + + if active: + vl_name = active.layer + else: + vl_name = rlayers[-1].layer # -1 : bottom node == upper layer + + if ' / ' in vl_name: + obname, lname = vl_name.split(' / ') + lname = bpy.path.clean_name(lname) + base_path = f'//render/{bpy.path.clean_name(obname)}' + slot_name = f'{lname}/{lname}_' + else: + # directly use full vlname for both base output and subfolder ?? (or return error) + obname = lname = bpy.path.clean_name(vl_name) + base_path = f'//render/' + slot_name = f'{lname}/{lname}_' + # change colors of those nodes + disconnected_groups = [] color = fn.random_color() for n in rlayers: n.use_custom_color = True n.color = color - obname, lname = vl_name.split(' / ') - lname = bpy.path.clean_name(lname) + if disconnect: + if n.outputs[0].is_linked: + for lnk in reversed(n.outputs[0].links): + if lnk.to_node.name.startswith('NG_'): + disconnected_groups.append(lnk.to_node) + links.remove(lnk) + disconnected_groups = list(set(disconnected_groups)) + ng_name = f'merge_NG_{obname}' # only object name - - ## clear nodes groups duplication (.00?) + ## clear unused nodes groups duplication fn.clear_nodegroup(ng_name, full_clear=False) - # get set nodegroup from vlayer name - if not ng: - ng = nodes.get(ng_name) + ### always create a new nodegroup (nerve call an existing one) - 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) + # need a unique nodegroup name + # increment name while nodegroup exists + while bpy.data.node_groups.get(ng_name): # nodes.get(ng_name) + if not re.search(r'(\d+)$', ng_name): + ng_name += '_02' # if not ending with a number add _02 + ng_name = re.sub(r'(\d+)(?!.*\d)', lambda x: str(int(x.group(1))+1).zfill(len(x.group(1))), ng_name) - if not ngroup: - # delete and recreate ? - print(f'create nodegroup {ng_name}') - ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree') + print(f'create merge nodegroup {ng_name}') + ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree') + ng = fn.create_node('CompositorNodeGroup', tree=node_tree, location=(fn.real_loc(rlayers[0]).x + 1900, fn.real_loc(rlayers[0]).y - 200), width=400) + ng.node_tree = ngroup + ng.name = ngroup.name - 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)) - ng_in = fn.create_node('NodeGroupInput', tree=ngroup, location=(-600,0)) - ng_out = fn.create_node('NodeGroupOutput', tree=ngroup, location=(600,0)) + # Create inputs and links to node_group + for rln in rlayers: + rln.outputs['Image'] + sockin = ng.inputs.new('NodeSocketColor', rln.layer) + sockin = ng.inputs[-1] + links.new(rln.outputs['Image'], sockin) + fn.nodegroup_merge_inputs(ng.node_tree) + ng.update() + # create dedicated fileout - - """ - socket_list = [] - grp_sockets = [] - - for n in rlayers: - if n.outputs[0].links[0].to_node != ng: - print(f'Skip {n.layer}, connect to {n.outputs[0].links[0].to_node} instead of {ng.name}') - continue - - sock_in = n.outputs[0].links[0].to_socket - for i, s in enumerate(ng.inputs): - if s == sock_in: - print(i, s.name) - socket_list.append(s) - grp_sockets.append(ng.node_tree.nodes['Group Input'].outputs[i]) - break + out = fn.create_node('CompositorNodeOutputFile', tree=node_tree, location=(ng.location[0]+450, ng.location[1]+50), width=600) + out_name = f'merge_OUT_{vl_name}' # or get output from frame + out.name = out_name + out.base_path = base_path + out.file_slots.new(slot_name) + links.new(ng.outputs[0], out.inputs[-1]) - # debug - for inp, grps in zip(socket_list, grp_sockets): - if inp.name != grps.name: - print(f'\n! Problem ! : {inp.name}, {grps.name}') - return - """ + fn.clear_disconnected(out) + out.update() - ## - # JUST CREATE ANOTHER GROUP NODE FOR THE MERGE ! - ## + ## Clear node_group after disconnect + # for dg in disconnected_groups: + # fn.clean_nodegroup_inputs(dg) + # # fn.clear_nodegroup_content_if_disconnected(dg.node_tree) + + return ng, out class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): bl_idname = "gp.merge_selected_dopesheet_layers" @@ -100,6 +115,8 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type == 'GPENCIL' + disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) + def execute(self, context): # merge_selected_layers() # function to merge from GP dopesheet ob = bpy.context.object @@ -130,7 +147,7 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): rlayers.append(rlayer) - merge_layers(rlayers, obname=clean_ob_name) + merge_layers(rlayers, disconnect=self.disconnect) return {"FINISHED"} @@ -138,9 +155,11 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): 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_description = "Merge selected view layers to a new dedicated file output\nDisconnect single output unless using 'keep connect'" bl_options = {"REGISTER"} + disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) + def execute(self, context): render = bpy.data.scenes.get('Render') if not render: @@ -159,7 +178,7 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): print('Merge -> Not every nodes start with the same object') # obname = selection[0].layer.split('.')[0] - merge_layers(selection, nodes.active) + merge_layers(selection, active=nodes.active, disconnect=self.disconnect) return {"FINISHED"} diff --git a/OP_number_outputs.py b/OP_number_outputs.py new file mode 100644 index 0000000..df05adc --- /dev/null +++ b/OP_number_outputs.py @@ -0,0 +1,60 @@ +import bpy +from . import fn + +class GPEXP_OT_number_outputs(bpy.types.Operator): + bl_idname = "gp.number_outputs" + bl_label = "Number Outputs" + bl_description = "(Re)Number the outputs to have ordered file by name in export directories\nCtrl+Clic : Delete numbering" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + mode : bpy.props.StringProperty(default='SELECTED', options={'SKIP_SAVE'}) + # ctrl : bpy.props.StringProperty(default=False, options={'SKIP_SAVE'}) # no need + + def invoke(self, context, event): + self.ctrl = event.ctrl + return self.execute(context) + + def execute(self, context): + render = bpy.data.scenes.get('Render') + if not render: + print('SKIP, no Render scene') + return {"CANCELLED"} + + ct = 0 + nodes = render.node_tree.nodes + for fo in nodes: + if fo.type != 'OUTPUT_FILE': + continue + if self.mode == 'SELECT' and not fo.select: + continue + print(f'numbering {fo.name}') + ct += 1 + if self.ctrl: + fn.delete_numbering(fo) + else: + fn.renumber(fo) + + txt = 'de-numbered' if self.ctrl else 're-numbered' + if ct: + self.report({'INFO'}, f'{ct} output nodes {txt}') + else: + self.report({'ERROR'}, f'No output nodes {txt}') + + return {"FINISHED"} + + +classes=( +GPEXP_OT_number_outputs, +) + +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/fn.py b/fn.py index de75b33..3470092 100644 --- a/fn.py +++ b/fn.py @@ -35,7 +35,9 @@ 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 ? + scene.render.film_transparent = True + scene.view_settings.view_transform = 'Standard' + def get_render_scene(): '''Get / Create a scene named Render''' @@ -169,6 +171,10 @@ def clear_nodegroup(name, full_clear=False): for ng in reversed(bpy.data.node_groups): pattern = name + r'\.\d{3}' + + if not full_clear and ng.users: + continue + if re.search(pattern, ng.name): bpy.data.node_groups.remove(ng) @@ -242,6 +248,17 @@ def reorder_fileout(fo, ng=None): all_outnames = [s.name for s in fo.inputs] # same as [fs.path for fs in fo.file_slots] fo.inputs.move(all_outnames.index(s_name), ordered.index(s_name)) +def reorganise_NG_nodegroup(ng): + '''refit node content to avoid overlap''' + ngroup = ng.node_tree + ng_in = ngroup.nodes.get('Group Input') + offset = 35 + y = 0 + for s in ng_in.outputs: + if s.is_linked: + s.links[0].to_node.location.y = y + y -= offset + def connect_to_group_output(n): for o in n.outputs: if o.is_linked: @@ -262,8 +279,177 @@ def connect_to_group_input(n): return val return False + +def clear_nodegroup_content_if_disconnected(ngroup): + '''Get a nodegroup.node_tree + delete orphan nodes that are not connected from group input node + ''' + for n in reversed(ngroup.nodes): + if n.type in ('GROUP_INPUT', 'GROUP_OUTPUT'): + continue + if not connect_to_group_input(n) and not connect_to_group_output(n): # is disconnected from both side + ngroup.nodes.remove(n) + +def clean_nodegroup_inputs(ng): + '''Clear inputs to output of passed nodegroup if not connected''' + ngroup = ng.node_tree + for i in range(len(ng.inputs))[::-1]: + print(i) + if not ng.inputs[i].is_linked: # and not any(x.layer == s.name for x in rl_nodes) + ngroup.inputs.remove(ngroup.inputs[i]) + + # clear_nodegroup_content_if_disconnected(ngroup) + 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 + return (random.uniform(0,1), random.uniform(0,1), random.uniform(0,1)) + + +def get_numbered_output(out, slot_name): + '''Return output slot name without looking for numbering ???_ + ''' + pattern = r'^(?:\d{3}_)?' # optional non capture group of 3 digits + _ + pattern = f'{pattern}{slot_name}' + for inp in out.inputs: + if re.match(pattern, inp.name): + return inp + + +def add_fileslot_number(fs, number): + elems = fs.path.split('/') + for i, e in enumerate(elems): + if re.match(r'^\d{3}_', e): + elems[i] = re.sub(r'^(\d{3})', lambda x: str(number).zfill(3), e) + else: + elems[i] = f'{str(number).zfill(3)}_{e}' + new = '/'.join(elems) + fs.path = new + return new + +def renumber(fo, offset=10): + '''Force renumber all the slots with a 3''' + + if fo.type != 'OUTPUT_FILE': return + ct = 0 + for fs in fo.file_slots: + add_fileslot_number(fs, ct) + ct += offset + +def get_num(string): + num = re.search(r'^(\d{3})_', string) + if num: + return int(num.group(1)) +""" +def renumber_keep(fo, offset=10): + '''Force renumber all the slots with a 3''' + + if fo.type != 'OUTPUT_FILE': return + + renum = re.compile(r'^(\d{3})_') + ct = 0 + + last_idx = len(fo.file_slots) -1 + prev = None + prev_num = None + for idx, fs in enumerate(fo.file_slots): + # if idx!=last_idx: + # fs_next = fo.file_slots[idx+1] + if idx == last_idx: # handle last + if idx > 0: + prev = fo.file_slots[idx-1] + num = renum.search(prev) + if not num: + add_fileslot_number(fs, ct) + else: + add_fileslot_number(fs, int(num.group(1)) + offset) + else: + add_fileslot_number(fs, 0) # there is only one slot (maybe don't number ?) + break + + # update the ct with the current taken number if any + number = renum.search(fs.path) + if number: + prev = fs + ct = number + offset + continue # skip already numbered + + # analyse all next slots until there is numbered + divider = None + for i in range(idx + 1, len(fo.file_slots) - idx): + next_num = renum.search(fo.file_slots[idx + i]) + if next_num: + divider = i-1 + break + + if idx == 0: + prev_num = 0 + prev = None + else: + prev = fo.file_slots[idx-1] + prev_num = renum.search(prev.path) + if prev_num: + prev_num = prev_num.group(1) + + if not divider: # just use prev and next if prev + if not prev: + add_fileslot_number(fs, ct) + + # first check if it has a number (if not bas) + prev = fs + ct += offset +""" + +def delete_numbering(fo): # padding=3 + '''Delete prefix numbering on all slots on passed file output''' + + if fo.type != 'OUTPUT_FILE': return + for fs in fo.file_slots: + elems = fs.path.split('/') + for i, e in enumerate(elems): + elems[i] = re.sub(r'^\d{3}_', '', e) + + new = '/'.join(elems) + fs.path = new + + + +def nodegroup_merge_inputs(ngroup): + '''Get a nodegroup + merge every group inputs with alpha over + then connect to antialias and a new output + ''' + + ng_in = ngroup.nodes.get('Group Input') + ng_out = ngroup.nodes.get('Group Output') + + x, y = ng_in.location.x + 200, 0 + + offset_x, offset_y = 150, -100 + + # merge all inputs in alphaover nodes + prev = None + for i in range(len(ng_in.outputs)-1): # skip waiting point + inp = ng_in.outputs[i] + if not prev: + prev = ng_in + continue + + # live connect + ao = create_node('CompositorNodeAlphaOver', tree=ngroup, location=(x,y), hide=True) + ngroup.links.new(prev.outputs[0], ao.inputs[1]) + ngroup.links.new(inp, ao.inputs[2]) + + x += offset_x + y += offset_y + prev = ao + + ## create a merged name as output ?? + aa = new_aa_node(ngroup) + aa.location = (ao.location.x + 200, ao.location.y) + ngroup.links.new(ao.outputs[0], aa.inputs[0]) # node_tree + + # create one input and link + out = ngroup.outputs.new('NodeSocketColor', ngroup.inputs[0].name) + ngroup.links.new(aa.outputs[0], ng_out.inputs[0]) \ No newline at end of file diff --git a/gen_vlayer.py b/gen_vlayer.py index 987a5d9..a9f0e69 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -106,7 +106,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): print('creating socket', vl_name) sockin = ng.inputs.new('NodeSocketColor', vl_name) sockin = ng.inputs[-1] - + links.new(rlayer.outputs['Image'], sockin) ## get nodes from frame @@ -124,8 +124,8 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): ## reorder fn.reorder_inputs(ng) ng.update() + # CREATE NG outsocket (individual, without taking merge) - connected = False if ng_in.outputs[vl_name].is_linked: @@ -152,6 +152,8 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): ngroup.links.new(ng_in.outputs[vl_name], aa.inputs[0]) # node_tree ngroup.links.new(aa.outputs[0], ng_out.inputs[vl_name]) # node_tree + fn.reorganise_NG_nodegroup(ng) # decorative + # clean outputs for o in reversed(ngroup.outputs): if not o.name in [o.name for o in ngroup.inputs]: @@ -182,12 +184,16 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): out = nodes.get(out_name) if not out: # color = (0.2,0.3,0.5) - out = fn.create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(fn.real_loc(ng)[0]+600, fn.real_loc(ng)[1]+50), width=600) # =(ng.location[0]+600, ng.location[1]+50) + out = fn.create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(fn.real_loc(ng)[0]+500, fn.real_loc(ng)[1]+50), width=600) # =(ng.location[0]+600, ng.location[1]+50) out.name = out_name out.parent = frame out.base_path = f'//render/{bpy.path.clean_name(obname)}' - out_input = out.inputs.get(slot_name) + ## out_input = out.inputs.get(slot_name) # ok for non-numbered outputs + + out_input = None + out_input = fn.get_numbered_output(out, slot_name) + if not out_input: out.file_slots.new(slot_name) out_input = out.inputs[-1] # assigning directly above doesn't link afterwards @@ -197,7 +203,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): links.new(groupout, out_input) # clean fileout - fn.clear_disconnected(out) + fn.clear_disconnected(out) # maybe not disconnect ? fn.reorder_fileout(out, ng=ng) out.update() diff --git a/standalone_scripts/rn_full_clear.py b/standalone_scripts/rn_full_clear.py index 65e73fb..65b9b7f 100644 --- a/standalone_scripts/rn_full_clear.py +++ b/standalone_scripts/rn_full_clear.py @@ -10,6 +10,7 @@ def clear(): render = bpy.data.scenes.get('Render') if not render: print('SKIP, no Render scene') + return {"CANCELLED"} # # clear passes # for i in range(len(render.view_layers))[::-1]: diff --git a/ui.py b/ui.py index 1c0e330..ff912e7 100644 --- a/ui.py +++ b/ui.py @@ -11,26 +11,50 @@ class GPEXP_PT_gp_node_ui(Panel): bl_label = "Gpencil Render Manager" def draw(self, context): + if not context.scene.use_nodes or not context.scene.node_tree: + return + + # TODO : add advanced bool checkbox to hide some options from the user + layout = self.layout - - row = layout.row() + col = layout.column(align=True) 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 + col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text=txt).disconnect = True + col.operator('gp.merge_selected_viewlayer_nodes', icon='NODETREE', text='Merge (keep connect)').disconnect = False + col.enabled = ct > 1 layout.separator() + layout.label(text='All Outputs:') 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.label(text='Clean and updates:') + + row = col.row() + n = context.scene.node_tree.nodes.active + row.enabled = n and n.type == 'R_LAYERS' and not n.outputs[0].is_linked + row.operator('gp.reconnect_render_layer', icon='ANIM', text='Reconnect') + + col.operator('gp.clean_compo_tree', icon='BRUSHES_ALL', text='Clean Nodes') # NODE_CORNER + + col.separator() + col.operator('gp.number_outputs', icon='LINENUMBERS_ON', text='Renumber outputs') + ## maybe get only this one... + col.operator('gp.number_outputs', icon='LINENUMBERS_ON', text='Renumber Selected outputs').mode = 'SELECTED' + layout.separator() col=layout.column() + col.label(text='Delete Options:') 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.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'