import os from typing import Optional, TextIO import bpy from lnx.exporter import LeenkxExporter import lnx.log import lnx.node_utils import lnx.utils if lnx.is_reload(__name__): lnx.exporter = lnx.reload_module(lnx.exporter) from lnx.exporter import LeenkxExporter lnx.log = lnx.reload_module(lnx.log) lnx.node_utils = lnx.reload_module(lnx.node_utils) lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) parsed_nodes = [] parsed_ids = dict() # Sharing node data function_nodes = dict() function_node_outputs = dict() group_name = '' def get_logic_trees() -> list['lnx.nodes_logic.LnxLogicTree']: ar = [] for node_group in bpy.data.node_groups: if node_group.bl_idname == 'LnxLogicTreeType': node_group.use_fake_user = True # Keep fake references for now ar.append(node_group) return ar # Generating node sources def build(): os.chdir(lnx.utils.get_fp()) trees = get_logic_trees() if len(trees) > 0: # Make sure package dir exists nodes_path = 'Sources/' + lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_package).replace(".", "/") + "/node" if not os.path.exists(nodes_path): os.makedirs(nodes_path) # Export node scripts for tree in trees: build_node_tree(tree) def build_node_tree(node_group: 'lnx.nodes_logic.LnxLogicTree'): global parsed_nodes global parsed_ids global function_nodes global function_node_outputs global group_name parsed_nodes = [] parsed_ids = dict() function_nodes = dict() function_node_outputs = dict() root_nodes = get_root_nodes(node_group) pack_path = lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_package) path = 'Sources/' + pack_path.replace('.', '/') + '/node/' group_name = lnx.node_utils.get_export_tree_name(node_group, do_warn=True) file = path + group_name + '.hx' if node_group.lnx_cached and os.path.isfile(file): return wrd = bpy.data.worlds['Lnx'] with open(file, 'w', encoding="utf-8") as f: f.write('package ' + pack_path + '.node;\n\n') f.write('@:access(leenkx.logicnode.LogicNode)') f.write('@:keep class ' + group_name + ' extends leenkx.logicnode.LogicTree {\n\n') f.write('\tvar functionNodes:Map;\n\n') f.write('\tvar functionOutputNodes:Map;\n\n') f.write('\tpublic function new() {\n') f.write('\t\tsuper();\n') if wrd.lnx_debug_console: f.write('\t\tname = "' + group_name + '";\n') f.write('\t\tthis.functionNodes = new Map();\n') f.write('\t\tthis.functionOutputNodes = new Map();\n') if lnx.utils.is_livepatch_enabled(): # Store a reference to this trait instance in Logictree.nodeTrees f.write('\t\tvar nodeTrees = leenkx.logicnode.LogicTree.nodeTrees;\n') f.write(f'\t\tif (nodeTrees.exists("{group_name}")) ' + '{\n') f.write(f'\t\t\tnodeTrees["{group_name}"].push(this);\n') f.write('\t\t} else {\n') f.write(f'\t\t\tnodeTrees["{group_name}"] = cast [this];\n') f.write('\t\t}\n') f.write('\t\tnotifyOnRemove(() -> { nodeTrees.remove("' + group_name + '"); });\n') f.write('\t\tnotifyOnAdd(add);\n') f.write('\t}\n\n') f.write('\toverride public function add() {\n') for node in root_nodes: build_node(node, f) f.write('\t}\n') # Create node functions for node_name in function_nodes: node = function_nodes[node_name] function_name = node.function_name f.write('\n\tpublic function ' + function_name + '(') for i in range(0, len(node.outputs) - 1): if i != 0: f.write(', ') f.write('arg' + str(i) + ':Dynamic') f.write(') {\n') f.write('\t\tvar functionNode = this.functionNodes["' + node_name + '"];\n') f.write('\t\tfunctionNode.args = [];\n') for i in range(0, len(node.outputs) - 1): f.write('\t\tfunctionNode.args.push(arg' + str(i) + ');\n') f.write('\t\tfunctionNode.run(0);\n') if function_node_outputs.get(function_name) != None: f.write('\t\treturn this.functionOutputNodes["' + function_node_outputs[function_name] + '"].result;\n') f.write('\t}\n\n') f.write('}') node_group.lnx_cached = True def build_node_group_tree(node_group: 'lnx.nodes_logic.LnxLogicTree', f: TextIO, group_node_name: str): """Builds the given node tree as a node group""" root_nodes = get_root_nodes(node_group) group_input_name = "" group_output_name = "" tree_name = lnx.node_utils.get_export_tree_name(node_group) # Get names of group input and out nodes if they exist for node in node_group.nodes: if node.bl_idname == 'LNGroupInputsNode': group_input_name = group_node_name + '_' + tree_name + lnx.node_utils.get_export_node_name(node) if node.bl_idname == 'LNGroupOutputsNode': group_output_name = group_node_name + '_' + tree_name + lnx.node_utils.get_export_node_name(node) for node in root_nodes: build_node(node, f, group_node_name + '_' + tree_name) node_group.lnx_cached = True return group_input_name, group_output_name def build_node(node: bpy.types.Node, f: TextIO, name_prefix: str = None) -> Optional[str]: """Builds the given node and returns its name. f is an opened file object.""" global parsed_nodes global parsed_ids use_live_patch = lnx.utils.is_livepatch_enabled() link_group = False if node.type == 'REROUTE': if len(node.inputs) > 0 and len(node.inputs[0].links) > 0: return build_node(node.inputs[0].links[0].from_node, f) else: return None # Get node name name = lnx.node_utils.get_export_node_name(node) if name_prefix is not None: name = name_prefix + name # Check and parse group nodes if they exist if node.bl_idname == 'LNCallGroupNode': prop = node.group_tree if prop is not None: group_input_name, group_output_name = build_node_group_tree(prop, f, name) link_group = True # Link tree variable nodes using IDs if node.lnx_logic_id != '': parse_id = node.lnx_logic_id if name_prefix is not None: parse_id = name_prefix + parse_id if parse_id in parsed_ids: return parsed_ids[parse_id] parsed_ids[parse_id] = name # Check if node already exists if name in parsed_nodes: # Check if node groups were parsed if not link_group: return name else: return group_output_name parsed_nodes.append(name) if not link_group: # Create node node_type = node.bl_idname[2:] # Discard 'LN' prefix f.write('\t\tvar ' + name + ' = new leenkx.logicnode.' + node_type + '(this);\n') # Handle Function Nodes if no node groups exist if node_type == 'FunctionNode' and name_prefix is None: f.write('\t\tthis.functionNodes.set("' + name + '", ' + name + ');\n') function_nodes[name] = node elif node_type == 'FunctionOutputNode' and name_prefix is None: f.write('\t\tthis.functionOutputNodes.set("' + name + '", ' + name + ');\n') # Index function output name by corresponding function name function_node_outputs[node.function_name] = name wrd = bpy.data.worlds['Lnx'] # Watch in debug console if node.lnx_watch and wrd.lnx_debug_console: f.write('\t\t' + name + '.name = "' + name[1:] + '";\n') f.write('\t\t' + name + '.watch(true);\n') elif use_live_patch: f.write('\t\t' + name + '.name = "' + name[1:] + '";\n') f.write(f'\t\tthis.nodes["{name[1:]}"] = {name};\n') # Properties for prop_py_name, prop_hx_name in lnx.node_utils.get_haxe_property_names(node): prop = lnx.node_utils.haxe_format_prop_value(node, prop_py_name) f.write('\t\t' + name + '.' + prop_hx_name + ' = ' + prop + ';\n') # Avoid unnecessary input/output array resizes f.write(f'\t\t{name}.preallocInputs({len(node.inputs)});\n') f.write(f'\t\t{name}.preallocOutputs({len(node.outputs)});\n') # Create inputs if link_group: # Replace Call Node Group Node name with Group Input Node name name = group_input_name for idx, inp in enumerate(node.inputs): # True if the input is connected to a unlinked reroute # somewhere down the reroute line unconnected = False # Is linked -> find the connected node if inp.is_linked: n = inp.links[0].from_node socket = inp.links[0].from_socket # Follow reroutes first while n.type == "REROUTE": if len(n.inputs) == 0 or not n.inputs[0].is_linked: unconnected = True break socket = n.inputs[0].links[0].from_socket n = n.inputs[0].links[0].from_node if not unconnected: # Ignore warnings if "Any" socket type is used if inp.bl_idname != 'LnxAnySocket' and socket.bl_idname != 'LnxAnySocket': if (inp.bl_idname == 'LnxNodeSocketAction' and socket.bl_idname != 'LnxNodeSocketAction') or \ (socket.bl_idname == 'LnxNodeSocketAction' and inp.bl_idname != 'LnxNodeSocketAction'): lnx.log.warn(f'Sockets do not match in logic node tree "{group_name}": node "{node.name}", socket "{inp.name}"') inp_name = build_node(n, f, name_prefix) for i in range(0, len(n.outputs)): if n.outputs[i] == socket: inp_from = i from_type = lnx.node_utils.get_socket_type(socket) break # Not linked -> create node with default values else: inp_name = build_default_node(inp) inp_from = 0 from_type = lnx.node_utils.get_socket_type(inp) # The input is linked to a reroute, but the reroute is unlinked if unconnected: inp_name = build_default_node(inp) inp_from = 0 from_type = lnx.node_utils.get_socket_type(inp) # Add input f.write(f'\t\t{"var __link = " if use_live_patch else ""}leenkx.logicnode.LogicNode.addLink({inp_name}, {name}, {inp_from}, {idx});\n') if use_live_patch: to_type = lnx.node_utils.get_socket_type(inp) f.write(f'\t\t__link.fromType = "{from_type}";\n') f.write(f'\t\t__link.toType = "{to_type}";\n') f.write(f'\t\t__link.toValue = {lnx.node_utils.haxe_format_socket_val(inp.get_default_value())};\n') # Create outputs if link_group: # Replace Call Node Group Node name with Group Output Node name name = group_output_name for idx, out in enumerate(node.outputs): # Linked outputs are already handled after iterating over inputs # above, so only unconnected outputs are handled here if not out.is_linked: f.write(f'\t\t{"var __link = " if use_live_patch else ""}leenkx.logicnode.LogicNode.addLink({name}, {build_default_node(out)}, {idx}, 0);\n') if use_live_patch: out_type = lnx.node_utils.get_socket_type(out) f.write(f'\t\t__link.fromType = "{out_type}";\n') f.write(f'\t\t__link.toType = "{out_type}";\n') f.write(f'\t\t__link.toValue = {lnx.node_utils.haxe_format_socket_val(out.get_default_value())};\n') return name # Expects an output socket # It first checks all outgoing links for non-reroute nodes and adds them to a list # Then it recursively checks all the discoverey reroute nodes # Returns all non reroute nodes which are directly or indirectly connected to this output. def collect_nodes_from_output(out, f): outputs = [] reroutes = [] # skipped if there are no links for l in out.links: n = l.to_node if n.type == 'REROUTE': # collect all rerouts and process them later reroutes.append(n) else: # immediatly add the current node outputs.append(build_node(n, f)) for reroute in reroutes: for o in reroute.outputs: outputs = outputs + collect_nodes_from_output(o, f) return outputs def get_root_nodes(node_group): roots = [] for node in node_group.nodes: if node.bl_idname == 'NodeUndefined': lnx.log.warn('Undefined logic nodes in ' + node_group.name) return [] if node.type == 'FRAME': continue linked = False for out in node.outputs: if out.is_linked: linked = True break if not linked: # Assume node with no connected outputs as roots roots.append(node) return roots def build_default_node(inp: bpy.types.NodeSocket): """Creates a new node to give a not connected input socket a value""" is_custom_socket = isinstance(inp, lnx.logicnode.lnx_sockets.LnxCustomSocket) if is_custom_socket: # LnxCustomSockets need to implement get_default_value() default_value = inp.get_default_value() else: if hasattr(inp, 'default_value'): default_value = inp.default_value else: default_value = None default_value = lnx.node_utils.haxe_format_socket_val(default_value, array_outer_brackets=False) inp_type = lnx.node_utils.get_socket_type(inp) if inp_type == 'VECTOR': return f'new leenkx.logicnode.VectorNode(this, {default_value})' elif inp_type == 'ROTATION': # a rotation is internally represented as a quaternion. return f'new leenkx.logicnode.RotationNode(this, {default_value})' elif inp_type in ('RGB', 'RGBA'): return f'new leenkx.logicnode.ColorNode(this, {default_value})' elif inp_type == 'VALUE': return f'new leenkx.logicnode.FloatNode(this, {default_value})' elif inp_type == 'INT': return f'new leenkx.logicnode.IntegerNode(this, {default_value})' elif inp_type == 'BOOLEAN': return f'new leenkx.logicnode.BooleanNode(this, {default_value})' elif inp_type == 'STRING': return f'new leenkx.logicnode.StringNode(this, {default_value})' elif inp_type == 'NONE': return 'new leenkx.logicnode.NullNode(this)' elif inp_type == 'OBJECT': return f'new leenkx.logicnode.ObjectNode(this, {default_value})' elif is_custom_socket: return f'new leenkx.logicnode.DynamicNode(this, {default_value})' else: return 'new leenkx.logicnode.NullNode(this)'