import bpy import re from mathutils import Vector from pathlib import Path from math import isclose from collections import defaultdict def create_node(type, tree=None, **kargs): '''Get a type, a tree to add in, and optionnaly multiple attribute to set return created node ''' tree = tree or bpy.context.scene.node_tree node = tree.nodes.new(type) for k,v in kargs.items(): setattr(node, k, v) return node def new_aa_node(tree, **kargs): '''create AA node''' aa = create_node('CompositorNodeAntiAliasing', tree) # type = ANTIALIASING aa.threshold = 1.0 # 0.5 aa.contrast_limit = 0.25 # 0.5 aa.corner_rounding = 0.25 aa.hide = True for k,v in kargs.items(): setattr(aa, k, v) return aa def create_aa_nodegroup(tree): ngroup = bpy.data.node_groups.get('AA') if not ngroup: ngroup = bpy.data.node_groups.new('AA', 'CompositorNodeTree') ng_in = create_node('NodeGroupInput', tree=ngroup, location=(-600,0)) ng_out = create_node('NodeGroupOutput', tree=ngroup, location=(600,0)) sep = create_node('CompositorNodeSepRGBA', tree=ngroup, location=(-150,0)) comb = create_node('CompositorNodeCombRGBA', tree=ngroup, location=(350,25)) # in AA # ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) # <- connect without out AA aa = new_aa_node(ngroup, location=(-400, 0)) # ngroup.links.new(ng_in.outputs[0], sep.inputs[0]) ngroup.links.new(ng_in.outputs[0], aa.inputs[0]) ngroup.links.new(aa.outputs[0], sep.inputs[0]) # ngroup.links.new(ng_in.outputs[0], sep.inputs[0]) for i in range(3): ngroup.links.new(sep.outputs[i], comb.inputs[i]) # alpha AA alpha_aa = new_aa_node(ngroup, location=(100,-150)) ngroup.links.new(sep.outputs[3], alpha_aa.inputs[0]) ngroup.links.new(alpha_aa.outputs[0], comb.inputs[3]) ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) ng = create_node('CompositorNodeGroup', tree=tree) ng.node_tree = ngroup ng.name = ngroup.name ng.hide=True return ng def copy_settings(obj_a, obj_b): exclusion = ['bl_rna', 'id_data', 'identifier','name_property','rna_type','properties', 'stamp_note_text','use_stamp_note', 'settingsFilePath', 'settingsStamp', 'select', 'matrix_local', 'matrix_parent_inverse', 'matrix_basis','location','rotation_euler', 'rotation_quaternion', 'rotation_axis_angle', 'scale'] for attr in dir(obj_a): if attr.startswith('__'): continue if attr in exclusion: continue # print('attr: ', attr) # if obj_a.is_property_readonly(attr): # block when things aren't attribute # continue try: val = getattr(obj_a, attr) except AttributeError: # print(f'cant get {attr}') pass try: setattr(obj_b, attr, val) except: # print(f"can't set {attr}") pass def set_file_output_format(fo): fo.format.color_mode = 'RGBA' fo.format.file_format = 'PNG' fo.format.color_depth = '8' fo.format.compression = 15 def set_settings(scene=None): if not scene: scene = bpy.context.scene # specify scene settings for these kind of render scene.eevee.taa_render_samples = 1 scene.grease_pencil_settings.antialias_threshold = 0 scene.render.film_transparent = True scene.view_settings.view_transform = 'Standard' def get_render_scene(): '''Get / Create a scene named Render''' render_scn = bpy.data.scenes.get('Render') if not render_scn: current = bpy.context.scene render_scn = bpy.data.scenes.new('Render') ## copy original settings over to new scene # copy_settings(current, render_scn) # BAD for attr in ['frame_start', 'frame_end', 'frame_current', 'camera', 'world']: setattr(render_scn, attr, getattr(current, attr)) copy_settings(current.render, render_scn.render) ## link cameras (and lights ?) for ob in bpy.context.scene.objects: if ob.type in ('CAMERA', 'LIGHT'): render_scn.collection.objects.link(ob) render_scn.render.filepath = '//render/preview/preview_' # set adapted render settings (no AA) set_settings(render_scn) render_scn.use_nodes = True return render_scn def get_view_layer(name, scene=None): '''get viewlayer name return existing/created viewlayer ''' if not scene: scene = get_render_scene() ### pass double letter prefix as suffix ## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name') ## pass_name = f'{name}_{passe}' pass_vl = scene.view_layers.get(name) if not pass_vl: pass_vl = scene.view_layers.new(name) return pass_vl ### node location tweaks def real_loc(n): if not n.parent: return n.location return n.location + real_loc(n.parent) def get_frame_transform(f, node_tree=None): '''Return real transform location of a frame node only works with one level of nesting (not recursive) ''' if not node_tree: node_tree = f.id_data if f.type != 'FRAME': return # return real_loc(f), f.dimensions childs = [n for n in node_tree.nodes if n.parent == f] # real_locs = [f.location + n.location for n in childs] xs = [n.location.x for n in childs] + [n.location.x + n.dimensions.x for n in childs] ys = [n.location.y for n in childs] + [n.location.y - n.dimensions.y for n in childs] xs.sort(key=lambda loc: loc) # x val : ascending ys.sort(key=lambda loc: loc) # ascending # , reversed=True) # y val : descending loc = Vector((min(xs), max(ys))) dim = Vector((max(xs) - min(xs) + 60, max(ys) - min(ys) + 60)) return loc, dim ## get all frames with their real transform. def bbox(f, frames): xs=[] ys=[] for n in frames[f]: # nodes of passed frame # Better as Vectors ? if n.type == 'FRAME': if n not in frames.keys(): # print(f'frame {n.name} not in frame list') continue all_xs, all_ys = bbox(n, frames) # frames[n] xs += all_xs ys += all_ys else: loc = real_loc(n) xs += [loc.x, loc.x + n.dimensions.x] # + (n.dimensions.x/get_dpi_factor()) ys += [loc.y, loc.y - n.dimensions.y] # - (n.dimensions.y/get_dpi_factor()) # margin ~= 30 # return xs and ys return [min(xs)-30, max(xs)+30], [min(ys)-30, max(ys)+30] def get_frames_bbox(node_tree): '''Return a dic with all frames ex: {frame_node: (location, dimension), ...} ''' # create dic of frame object with his direct child nodes nodes frames = defaultdict(list) frames_bbox = {} for n in node_tree.nodes: if not n.parent: continue # also contains frames frames[n.parent].append(n) # Dic for bbox coord for f, nodes in frames.items(): if f.parent: continue xs, ys = bbox(f, frames) # xs, ys = bbox(nodes, frames) ## returning: list of corner coords # coords = [ # Vector((xs[0], ys[1])), # Vector((xs[1], ys[1])), # Vector((xs[1], ys[0])), # Vector((xs[0], ys[0])), # ] # frames_bbox[f] = coords ## returning: (loc vector, dimensions vector) frames_bbox[f] = Vector((xs[0], ys[1])), Vector((xs[1] - xs[0], ys[1] - ys[0])) return frames_bbox ## nodes helper functions def clear_nodegroup(name, full_clear=False): '''remove duplication of a nodegroup (.???) also remove the base one if full_clear True ''' 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) if full_clear and ng.name == name: # if full clear bpy.data.node_groups.remove(ng) def rearrange_frames(node_tree): frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...} if not frame_d: print('no frame found') return # print([f.name for f in frame_d.keys()]) ## order the dict by frame.y location frame_d = {key: value for key, value in sorted(frame_d.items(), key=lambda pair: pair[1][0].y - pair[1][1].y, reverse=True)} frames = [[f, v[0], v[1].y] for f, v in frame_d.items()] # [frame_node, real_loc, real dimensions] top = frames[0][1].y # upper node location.y # top = 0 #always start a 0 offset = 0 for f in frames: ## f[1] : real loc Vector ## f[0] : frame ## move frame by offset needed (delta between real_loc and "fake" loc , minus offset) f[0].location.y = (f[1].y - f[0].location.y) - offset # avoid offset when recalculating from 0 top # f[0].location.y = f[1].y - top - offset offset += f[2] + 200 # gap f[0].update() def reorder_inputs(ng): rl_nodes = [s.links[0].from_node for s in ng.inputs if s.is_linked and s.links and s.links[0].from_node.type == 'R_LAYERS'] rl_nodes.sort(key=lambda x: x.location.y, reverse=True) names = [n.layer for n in rl_nodes] inputs_names = [s.name for s in ng.inputs] filtered_names = [n for n in names if n in inputs_names] for dest, name in enumerate(filtered_names): ## rebuild list at each iteration so index are good inputs_names = [s.name for s in ng.inputs] src = inputs_names.index(name) # reorder on node_tree not directly on node! ng.node_tree.inputs.move(src, dest) def reorder_outputs(ng): ordered_out_name = [nis.name for nis in ng.inputs if nis.name in [o.name for o in ng.outputs]] for s_name in ordered_out_name: all_outnames = [o.name for o in ng.outputs] # reorder on nodetree, not on node ! ng.node_tree.outputs.move(all_outnames.index(s_name), ordered_out_name.index(s_name)) def clear_disconnected(fo): for inp in reversed(fo.inputs): if not inp.is_linked: print(f'Deleting unlinked fileout slot: {inp.name}') fo.inputs.remove(inp) def reorder_fileout(fo, ng=None): if not ng: # get connected nodegroup for s in fo.inputs: if s.is_linked and s.links and s.links[0].from_node.type == 'GROUP': ng = s.links[0].from_node break if not ng: print(f'No nodegroup to refer to filter {fo.name}') return ordered = [o.links[0].to_socket.name for o in ng.outputs if o.is_linked and o.is_linked and o.links[0].to_node == fo] for s_name in ordered: 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: if o.links[0].to_node.type == 'GROUP_OUTPUT': return o.links[0].to_socket val = connect_to_group_output(o.links[0].to_node) if val: return val return False def connect_to_group_input(n): for i in n.inputs: if i.is_linked: if i.links[0].from_node.type == 'GROUP_INPUT': return i.links[0].from_socket val = connect_to_group_input(i.links[0].from_node) if val: 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, skip_existing_pass=True): '''Clear inputs to output of passed nodegroup if not connected''' ngroup = ng.node_tree rl_nodes = [n.layer for n in ng.id_data.nodes if n.type == 'R_LAYERS'] for i in range(len(ng.inputs))[::-1]: if not ng.inputs[i].is_linked: if skip_existing_pass and any(ng.inputs[i].name == x for x in rl_nodes): # a render layer of this name still exists continue 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)) 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 = create_aa_nodegroup(ngroup) # 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]) ## --- renumbering funcs --- 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 = 10 # start at 10 for fs in fo.file_slots: add_fileslot_number(fs, ct) ct += offset def get_num(string) -> int: '''get a tring or a file_slot object return leading number or None ''' if not isinstance(string, str): string = string.path num = re.search(r'^(\d{3})_', string) if num: return int(num.group(1)) 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 reverse_fileout_inputs(fo): count = len(fo.inputs) for i in range(count): fo.inputs.move(count-1, i) def renumber_keep_existing(fo, offset=10, invert=True): '''Renumber by keeping existing numbers and inserting new one whenever possible Big and ugly function that do the trick nonetheless... ''' if fo.type != 'OUTPUT_FILE': return ct = 10 if invert: reverse_fileout_inputs(fo) fsl = fo.file_slots last_idx = len(fsl) - 1 prev = None prev_num = None for idx, fs in enumerate(fsl): # print('-->', idx, fs.path) if idx == last_idx: # handle last if get_num(fs) is not None: break if idx > 0: prev = fsl[idx-1] num = get_num(prev) if num is not None: add_fileslot_number(fs, num + offset) else: add_fileslot_number(fs, ct) else: add_fileslot_number(fs, 10) # there is only one slot (maybe don't number ?) break # update the ct with the current taken number if any number = get_num(fs) if number is not None: prev = fs ct = number + offset continue # skip already numbered # analyse all next slots until there is numbered divider = 0 # print(f'range(1, {len(fsl) - idx}') for i in range(1, len(fsl) - idx): next_num = get_num(fsl[idx + i]) if next_num is not None: divider = i+1 break if idx == 0: # handle first prev_num = 0 prev = None if next_num is None: add_fileslot_number(fs, 0) elif next_num == 0: print(f'Cannot insert value before 0 to {fsl.path}') continue else: add_fileslot_number(fs, int(next_num / 2)) else: prev = fsl[idx-1] test_prev = get_num(prev) if test_prev is not None: prev_num = test_prev if not divider: if prev_num is not None: add_fileslot_number(fs, prev_num + offset) else: add_fileslot_number(fs, ct) else: if prev_num is not None: # iterate rename gap_inc = int((next_num - prev_num) / divider) if gap_inc < 1: # same values ! print(f'cannot insert a median value at {fs.path} between {prev_num} and {next_num}') continue ct = prev_num for temp_id in range(idx, idx+i): ct += gap_inc add_fileslot_number(fsl[temp_id], ct) else: print("what's going on ?\n") # first check if it has a number (if not bas) prev = fs ct += offset if invert: reverse_fileout_inputs(fo) def has_channel_color(layer): '''Return True if gp_layer.channel_color is different than the default (0.2, 0.2, 0.2) ''' if not any(isclose(i, 0.2, abs_tol=0.001) for i in layer.channel_color): return True ## confirm pop-up message: def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def draw(self, context): for l in _message: if isinstance(l, str): self.layout.label(text=l) else: self.layout.label(text=l[0], icon=l[1]) if isinstance(_message, str): _message = [_message] bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)