1162 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			1162 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								from collections import OrderedDict
							 | 
						||
| 
								 | 
							
								import itertools
							 | 
						||
| 
								 | 
							
								import math
							 | 
						||
| 
								 | 
							
								import textwrap
							 | 
						||
| 
								 | 
							
								from typing import Any, final, Generator, List, Optional, Type, Union
							 | 
						||
| 
								 | 
							
								from typing import OrderedDict as ODict  # Prevent naming conflicts
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import bpy.types
							 | 
						||
| 
								 | 
							
								from bpy.props import *
							 | 
						||
| 
								 | 
							
								from nodeitems_utils import NodeItem
							 | 
						||
| 
								 | 
							
								from lnx.logicnode.lnx_sockets import LnxCustomSocket
							 | 
						||
| 
								 | 
							
								from mathutils import Vector
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import lnx  # we cannot import lnx.livepatch here or we have a circular import
							 | 
						||
| 
								 | 
							
								# Pass custom property types and NodeReplacement forward to individual
							 | 
						||
| 
								 | 
							
								# node modules that import lnx_nodes
							 | 
						||
| 
								 | 
							
								from lnx.logicnode.lnx_props import *
							 | 
						||
| 
								 | 
							
								from lnx.logicnode.replacement import NodeReplacement
							 | 
						||
| 
								 | 
							
								import lnx.node_utils
							 | 
						||
| 
								 | 
							
								import lnx.utils
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if lnx.is_reload(__name__):
							 | 
						||
| 
								 | 
							
								    lnx.logicnode.lnx_props = lnx.reload_module(lnx.logicnode.lnx_props)
							 | 
						||
| 
								 | 
							
								    from lnx.logicnode.lnx_props import *
							 | 
						||
| 
								 | 
							
								    lnx.logicnode.replacement = lnx.reload_module(lnx.logicnode.replacement)
							 | 
						||
| 
								 | 
							
								    from lnx.logicnode.replacement import NodeReplacement
							 | 
						||
| 
								 | 
							
								    lnx.node_utils = lnx.reload_module(lnx.node_utils)
							 | 
						||
| 
								 | 
							
								    lnx.utils = lnx.reload_module(lnx.utils)
							 | 
						||
| 
								 | 
							
								    lnx.logicnode.lnx_sockets = lnx.reload_module(lnx.logicnode.lnx_sockets)
							 | 
						||
| 
								 | 
							
								    from lnx.logicnode.lnx_sockets import LnxCustomSocket
							 | 
						||
| 
								 | 
							
								else:
							 | 
						||
| 
								 | 
							
								    lnx.enable_reload(__name__)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# When passed as a category to add_node(), this will use the capitalized
							 | 
						||
| 
								 | 
							
								# name of the package of the node as the category to make renaming
							 | 
						||
| 
								 | 
							
								# categories easier.
							 | 
						||
| 
								 | 
							
								PKG_AS_CATEGORY = "__pkgcat__"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								nodes = []
							 | 
						||
| 
								 | 
							
								category_items: ODict[str, List['LnxNodeCategory']] = OrderedDict()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								array_nodes: dict[str, 'LnxLogicTreeNode'] = dict()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# See LnxLogicTreeNode.update()
							 | 
						||
| 
								 | 
							
								# format: [tree pointer => (num inputs, num input links, num outputs, num output links)]
							 | 
						||
| 
								 | 
							
								last_node_state: dict[int, tuple[int, int, int, int]] = {}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxLogicTreeNode(bpy.types.Node):
							 | 
						||
| 
								 | 
							
								    lnx_category = PKG_AS_CATEGORY
							 | 
						||
| 
								 | 
							
								    lnx_section = 'default'
							 | 
						||
| 
								 | 
							
								    lnx_is_obsolete = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @final
							 | 
						||
| 
								 | 
							
								    def init(self, context):
							 | 
						||
| 
								 | 
							
								        # make sure a given node knows the version of the NodeClass from when it was created
							 | 
						||
| 
								 | 
							
								        if isinstance(type(self).lnx_version, int):
							 | 
						||
| 
								 | 
							
								            self.lnx_version = type(self).lnx_version
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.lnx_version = 1
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if not hasattr(self, 'lnx_init'):
							 | 
						||
| 
								 | 
							
								            # Show warning for older node packages
							 | 
						||
| 
								 | 
							
								            lnx.log.warn(f'Node {self.bl_idname} has no lnx_init function and might not work correctly!')
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            self.lnx_init(context)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_create', self)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def register_id(self):
							 | 
						||
| 
								 | 
							
								        """Registers a node ID so that the ID can be used by operators
							 | 
						||
| 
								 | 
							
								        to target this node (nodes can't be stored in pointer properties).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        array_nodes[self.get_id_str()] = self
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_id_str(self) -> str:
							 | 
						||
| 
								 | 
							
								        return str(self.as_pointer())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def poll(cls, ntree):
							 | 
						||
| 
								 | 
							
								        return ntree.bl_idname == 'LnxLogicTreeType' or 'LnxGroupTree'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def on_register(cls):
							 | 
						||
| 
								 | 
							
								        """Don't call this method register() as it will be triggered before Blender registers the class, resulting in
							 | 
						||
| 
								 | 
							
								        a double registration."""
							 | 
						||
| 
								 | 
							
								        add_node(cls, cls.lnx_category, cls.lnx_section, cls.lnx_is_obsolete)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def on_unregister(cls):
							 | 
						||
| 
								 | 
							
								        pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def absolute_location(cls, node):
							 | 
						||
| 
								 | 
							
								        """Gets the absolute location of the node including frames and parent nodes."""
							 | 
						||
| 
								 | 
							
								        locx, locy = node.location[:]
							 | 
						||
| 
								 | 
							
								        if node.parent:
							 | 
						||
| 
								 | 
							
								            locx += node.parent.location.x
							 | 
						||
| 
								 | 
							
								            locy += node.parent.location.y
							 | 
						||
| 
								 | 
							
								            return cls.absolute_location(node.parent)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return locx, locy
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_tree(self) -> bpy.types.NodeTree:
							 | 
						||
| 
								 | 
							
								        return self.id_data
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def getViewLocation(self):
							 | 
						||
| 
								 | 
							
								        node = self
							 | 
						||
| 
								 | 
							
								        location = node.location.copy()
							 | 
						||
| 
								 | 
							
								        while node.parent:
							 | 
						||
| 
								 | 
							
								            node = node.parent
							 | 
						||
| 
								 | 
							
								            location += node.location
							 | 
						||
| 
								 | 
							
								        return location
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def clear_tree_cache(self):
							 | 
						||
| 
								 | 
							
								        self.get_tree().lnx_cached = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def update(self):
							 | 
						||
| 
								 | 
							
								        """Called if the node was updated in some way, for example
							 | 
						||
| 
								 | 
							
								        if socket connections change. This callback is not called if
							 | 
						||
| 
								 | 
							
								        socket values were changed.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        def num_connected(sockets):
							 | 
						||
| 
								 | 
							
								            return sum([socket.is_linked for socket in sockets])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # If a link between sockets is removed, there is currently no
							 | 
						||
| 
								 | 
							
								        # _reliable_ way in the Blender API to check which connection
							 | 
						||
| 
								 | 
							
								        # was removed (*).
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # So instead we just check _if_ the number of links or sockets
							 | 
						||
| 
								 | 
							
								        # has changed (the update function is called before and after
							 | 
						||
| 
								 | 
							
								        # each link removal). Because we listen for those updates in
							 | 
						||
| 
								 | 
							
								        # general, we automatically also listen to link creation events,
							 | 
						||
| 
								 | 
							
								        # which is more stable than using the dedicated callback for
							 | 
						||
| 
								 | 
							
								        # that (`insert_link()`), because adding links can remove other
							 | 
						||
| 
								 | 
							
								        # links and we would need to react to that as well.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # (*) https://devtalk.blender.org/t/how-to-detect-which-link-was-deleted-by-user-in-node-editor
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self_id = self.as_pointer()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        current_state = (len(self.inputs), num_connected(self.inputs), len(self.outputs), num_connected(self.outputs))
							 | 
						||
| 
								 | 
							
								        if self_id not in last_node_state:
							 | 
						||
| 
								 | 
							
								            # Lazily initialize the last_node_state dict to also store
							 | 
						||
| 
								 | 
							
								            # state for nodes that already exist in the tree
							 | 
						||
| 
								 | 
							
								            last_node_state[self_id] = current_state
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if last_node_state[self_id] != current_state:
							 | 
						||
| 
								 | 
							
								            self.on_socket_state_change()
							 | 
						||
| 
								 | 
							
								            last_node_state[self_id] = current_state
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Notify sockets
							 | 
						||
| 
								 | 
							
								        for socket in itertools.chain(self.inputs, self.outputs):
							 | 
						||
| 
								 | 
							
								            if isinstance(socket, LnxCustomSocket):
							 | 
						||
| 
								 | 
							
								                socket.on_node_update()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def free(self):
							 | 
						||
| 
								 | 
							
								        """Called before the node is deleted."""
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_delete', self)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def copy(self, src_node):
							 | 
						||
| 
								 | 
							
								        """Called upon node duplication or upon pasting a copied node.
							 | 
						||
| 
								 | 
							
								        `self` holds the copied node and `src_node` a temporal copy of
							 | 
						||
| 
								 | 
							
								        the original node at the time of copying).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_copy', (self, src_node))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_prop_update(self, context: bpy.types.Context, prop_name: str):
							 | 
						||
| 
								 | 
							
								        """Called if a property created with a function from the
							 | 
						||
| 
								 | 
							
								        lnx_props module is changed. If the property has a custom update
							 | 
						||
| 
								 | 
							
								        function, it is called before `on_prop_update()`.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_update_prop', (self, prop_name))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_socket_val_update(self, context: bpy.types.Context, socket: bpy.types.NodeSocket):
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_socket_val', (self, socket))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_socket_state_change(self):
							 | 
						||
| 
								 | 
							
								        """Called if the state (amount, connection state) of the node's
							 | 
						||
| 
								 | 
							
								        socket changes (see LnxLogicTreeNode.update())
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.send_event('ln_update_sockets', self)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_logic_id_change(self):
							 | 
						||
| 
								 | 
							
								        """Called if the node's lnx_logic_id value changes."""
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								        lnx.live_patch.patch_export()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def insert_link(self, link: bpy.types.NodeLink):
							 | 
						||
| 
								 | 
							
								        """Called on *both* nodes when a link between two nodes is created."""
							 | 
						||
| 
								 | 
							
								        # lnx.live_patch.send_event('ln_insert_link', (self, link))
							 | 
						||
| 
								 | 
							
								        pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_replacement_node(self, node_tree: bpy.types.NodeTree):
							 | 
						||
| 
								 | 
							
								        # needs to be overridden by individual node classes with lnx_version>1
							 | 
						||
| 
								 | 
							
								        """(only called if the node's version is inferior to the node class's version)
							 | 
						||
| 
								 | 
							
								        Help with the node replacement process, by explaining how a node (`self`) should be replaced.
							 | 
						||
| 
								 | 
							
								        This method can either return a NodeReplacement object (see `nodes_logic.py`), or a brand new node.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If a new node is returned, then the following needs to be already set:
							 | 
						||
| 
								 | 
							
								        - the node's links to the other nodes
							 | 
						||
| 
								 | 
							
								        - the node's properties
							 | 
						||
| 
								 | 
							
								        - the node inputs's default values
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If more than one node need to be created (for example, if an input needs a type conversion after update),
							 | 
						||
| 
								 | 
							
								        please return all the nodes in a list.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        please raise a LookupError specifically when the node's version isn't handled by the function.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        note that the lowest 'defined' version should be 1. if the node's version is 0, it means that it has been saved before versioning was a thing.
							 | 
						||
| 
								 | 
							
								        NODES OF VERSION 1 AND VERSION 0 SHOULD HAVE THE SAME CONTENTS
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        if self.lnx_version == 0 and type(self).lnx_version == 1:
							 | 
						||
| 
								 | 
							
								            # In case someone doesn't implement this function, but the node has version 0
							 | 
						||
| 
								 | 
							
								            return NodeReplacement.Identity(self)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            raise LookupError(f"the current node class {repr(type(self)):s} does not implement get_replacement_node() even though it has updated")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def add_input(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Adds a new input socket to the node.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        socket = self.inputs.new(socket_type, socket_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if default_value is not None:
							 | 
						||
| 
								 | 
							
								            if isinstance(socket, LnxCustomSocket):
							 | 
						||
| 
								 | 
							
								                if socket.lnx_socket_type != 'NONE':
							 | 
						||
| 
								 | 
							
								                    socket.default_value_raw = default_value
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    raise ValueError('specified a default value for an input node that doesn\'t accept one')
							 | 
						||
| 
								 | 
							
								            else:  # should not happen anymore?
							 | 
						||
| 
								 | 
							
								                socket.default_value = default_value
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if is_var and not socket.display_shape.endswith('_DOT'):
							 | 
						||
| 
								 | 
							
								            socket.display_shape += '_DOT'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def add_output(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Adds a new output socket to the node.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        socket = self.outputs.new(socket_type, socket_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # FIXME: …a default_value on an output socket? Why is that a thing?
							 | 
						||
| 
								 | 
							
								        if default_value is not None:
							 | 
						||
| 
								 | 
							
								            if socket.lnx_socket_type != 'NONE':
							 | 
						||
| 
								 | 
							
								                socket.default_value_raw = default_value
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                raise ValueError('specified a default value for an input node that doesn\'t accept one')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if is_var and not socket.display_shape.endswith('_DOT'):
							 | 
						||
| 
								 | 
							
								            socket.display_shape += '_DOT'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_socket_index(self, socket:bpy.types.NodeSocket) -> int:
							 | 
						||
| 
								 | 
							
								        """Gets the scket index of a socket in this node."""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        index = 0
							 | 
						||
| 
								 | 
							
								        if socket.is_output:
							 | 
						||
| 
								 | 
							
								            for output in self.outputs:
							 | 
						||
| 
								 | 
							
								                if output == socket:
							 | 
						||
| 
								 | 
							
								                    return index
							 | 
						||
| 
								 | 
							
								                index = index + 1
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            for input in self.inputs:
							 | 
						||
| 
								 | 
							
								                if input == socket:
							 | 
						||
| 
								 | 
							
								                    return index
							 | 
						||
| 
								 | 
							
								                index = index + 1
							 | 
						||
| 
								 | 
							
								        return -1
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def insert_input(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Insert a new input socket to the node at a particular index.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        socket = self.add_input(socket_type, socket_name, default_value, is_var)
							 | 
						||
| 
								 | 
							
								        self.inputs.move(len(self.inputs) - 1, socket_index)
							 | 
						||
| 
								 | 
							
								        return socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def insert_output(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Insert a new output socket to the node at a particular index.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        socket = self.add_output(socket_type, socket_name, default_value, is_var)
							 | 
						||
| 
								 | 
							
								        self.outputs.move(len(self.outputs) - 1, socket_index)
							 | 
						||
| 
								 | 
							
								        return socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def change_input_socket(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Change an input socket type retaining the previous socket links
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        old_socket = self.inputs[socket_index]
							 | 
						||
| 
								 | 
							
								        links = old_socket.links
							 | 
						||
| 
								 | 
							
								        from_sockets = []
							 | 
						||
| 
								 | 
							
								        for link in links:
							 | 
						||
| 
								 | 
							
								            from_sockets.append(link.from_socket)
							 | 
						||
| 
								 | 
							
								        current_socket = self.insert_input(socket_type, socket_index, socket_name, default_value, is_var)
							 | 
						||
| 
								 | 
							
								        if default_value is None:
							 | 
						||
| 
								 | 
							
								            old_socket.copy_defaults(current_socket)
							 | 
						||
| 
								 | 
							
								        self.inputs.remove(old_socket)
							 | 
						||
| 
								 | 
							
								        tree = self.get_tree()
							 | 
						||
| 
								 | 
							
								        for from_socket in from_sockets:
							 | 
						||
| 
								 | 
							
								            tree.links.new(from_socket, current_socket)
							 | 
						||
| 
								 | 
							
								        return current_socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def change_output_socket(self, socket_type: str, socket_index: int, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
							 | 
						||
| 
								 | 
							
								        """Change an output socket type retaining the previous socket links
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        If `is_var` is true, a dot is placed inside the socket to denote
							 | 
						||
| 
								 | 
							
								        that this socket can be used for variable access (see
							 | 
						||
| 
								 | 
							
								        SetVariable node).
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        links = self.outputs[socket_index].links
							 | 
						||
| 
								 | 
							
								        to_sockets = []
							 | 
						||
| 
								 | 
							
								        for link in links:
							 | 
						||
| 
								 | 
							
								            to_sockets.append(link.to_socket)
							 | 
						||
| 
								 | 
							
								        self.outputs.remove(self.outputs[socket_index])
							 | 
						||
| 
								 | 
							
								        current_socket = self.insert_output(socket_type, socket_index, socket_name, default_value, is_var)
							 | 
						||
| 
								 | 
							
								        tree = self.get_tree()
							 | 
						||
| 
								 | 
							
								        for to_socket in to_sockets:
							 | 
						||
| 
								 | 
							
								            tree.links.new(current_socket, to_socket)
							 | 
						||
| 
								 | 
							
								        return current_socket
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxLogicVariableNodeMixin(LnxLogicTreeNode):
							 | 
						||
| 
								 | 
							
								    """A mixin class for variable nodes. This class adds functionality
							 | 
						||
| 
								 | 
							
								    that allows variable nodes to
							 | 
						||
| 
								 | 
							
								        1) be identified as such and
							 | 
						||
| 
								 | 
							
								        2) to be promoted to nodes that are linked to a tree variable.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    If a variable node is promoted to a tree variable node and the
							 | 
						||
| 
								 | 
							
								    tree variable does not exist yet, it is created. Each tree variable
							 | 
						||
| 
								 | 
							
								    only exists as long as there are variable nodes that are linked to
							 | 
						||
| 
								 | 
							
								    it. A variable node's links to a tree variables can be removed by
							 | 
						||
| 
								 | 
							
								    calling `make_local()`. If a tree variable node is copied to a
							 | 
						||
| 
								 | 
							
								    different tree where the variable doesn't exist, it is created.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Tree variable nodes come in two states: master and replica nodes.
							 | 
						||
| 
								 | 
							
								    In order to not having to find memory-intensive and complicated ways
							 | 
						||
| 
								 | 
							
								    for storing every possible variable node data in the tree variable
							 | 
						||
| 
								 | 
							
								    UI list entries themselves (Blender doesn't support dynamically
							 | 
						||
| 
								 | 
							
								    typed properties), we store the data in one of the variable nodes,
							 | 
						||
| 
								 | 
							
								    called the master node. The other nodes are synchronized with the
							 | 
						||
| 
								 | 
							
								    master node and must implement a routine to copy the data from the
							 | 
						||
| 
								 | 
							
								    master node.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    The user doesn't need to know about the master/replica concept, the
							 | 
						||
| 
								 | 
							
								    master node gets chosen automatically and it is made sure that there
							 | 
						||
| 
								 | 
							
								    can be only one master node, even after copying.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    is_master_node: BoolProperty(default=False)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    _text_wrapper = textwrap.TextWrapper()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def synchronize_from_master(self, master_node: 'LnxLogicVariableNodeMixin'):
							 | 
						||
| 
								 | 
							
								        """Called if the node should synchronize its data from the passed
							 | 
						||
| 
								 | 
							
								        master_node. Override this in variable nodes to react to updates
							 | 
						||
| 
								 | 
							
								        made to the master node.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _synchronize_to_replicas(self, master_node: 'LnxLogicVariableNodeMixin'):
							 | 
						||
| 
								 | 
							
								        for replica_node in self.get_replica_nodes(self.get_tree(), self.lnx_logic_id):
							 | 
						||
| 
								 | 
							
								            replica_node.synchronize_from_master(master_node)
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def make_local(self):
							 | 
						||
| 
								 | 
							
								        """Demotes this node to a local variable node that is not linked
							 | 
						||
| 
								 | 
							
								        to any tree variable.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        has_replicas = True
							 | 
						||
| 
								 | 
							
								        if self.is_master_node:
							 | 
						||
| 
								 | 
							
								            self._synchronize_to_replicas(self)
							 | 
						||
| 
								 | 
							
								            has_replicas = self.choose_new_master_node(self.get_tree(), self.lnx_logic_id)
							 | 
						||
| 
								 | 
							
								            self.is_master_node = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Remove the tree variable if there are no more nodes that link
							 | 
						||
| 
								 | 
							
								        # to it
							 | 
						||
| 
								 | 
							
								        if not has_replicas:
							 | 
						||
| 
								 | 
							
								            tree = self.get_tree()
							 | 
						||
| 
								 | 
							
								            for idx, item in enumerate(tree.lnx_treevariableslist):
							 | 
						||
| 
								 | 
							
								                if item.name == self.lnx_logic_id:
							 | 
						||
| 
								 | 
							
								                    tree.lnx_treevariableslist.remove(idx)
							 | 
						||
| 
								 | 
							
								                    break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            max_index = len(tree.lnx_treevariableslist) - 1
							 | 
						||
| 
								 | 
							
								            if tree.lnx_treevariableslist_index > max_index:
							 | 
						||
| 
								 | 
							
								                tree.lnx_treevariableslist_index = max_index
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        self.lnx_logic_id = ''
							 | 
						||
| 
								 | 
							
								        self.clear_tree_cache()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def free(self):
							 | 
						||
| 
								 | 
							
								        self.make_local()
							 | 
						||
| 
								 | 
							
								        super().free()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def copy(self, src_node: 'LnxLogicVariableNodeMixin'):
							 | 
						||
| 
								 | 
							
								        # Because the `copy()` callback is actually called upon pasting
							 | 
						||
| 
								 | 
							
								        # the node, `src_node` is a temporal copy of the copied node
							 | 
						||
| 
								 | 
							
								        # that retains the state of the node upon copying. This however
							 | 
						||
| 
								 | 
							
								        # means that we can't reliably use the master state of the
							 | 
						||
| 
								 | 
							
								        # pasted node because it might have changed in between, also
							 | 
						||
| 
								 | 
							
								        # `src_node.get_tree()` will return `None`. So if the pasted
							 | 
						||
| 
								 | 
							
								        # node is linked to a tree var, we simply check if the tree of
							 | 
						||
| 
								 | 
							
								        # the pasted node has the tree variable and depending on that we
							 | 
						||
| 
								 | 
							
								        # set `is_master_node`.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if self.lnx_logic_id != '':
							 | 
						||
| 
								 | 
							
								            target_tree = self.get_tree()
							 | 
						||
| 
								 | 
							
								            lst = target_tree.lnx_treevariableslist
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            self.is_master_node = False  # Ignore this node in get_master_node below
							 | 
						||
| 
								 | 
							
								            if self.__class__.get_master_node(target_tree, self.lnx_logic_id) is None:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                # copy() is not only called when manually copying/pasting
							 | 
						||
| 
								 | 
							
								                # nodes, but also when duplicating logic trees.
							 | 
						||
| 
								 | 
							
								                # In that case, Blender duplicates the lnx_treevariableslist
							 | 
						||
| 
								 | 
							
								                # property, so all tree variables already exist before
							 | 
						||
| 
								 | 
							
								                # adding a single node to the new tree. In turn, each
							 | 
						||
| 
								 | 
							
								                # tree variable exists without a master node before
							 | 
						||
| 
								 | 
							
								                # the first node referencing that variable is copied over
							 | 
						||
| 
								 | 
							
								                # to the new tree.
							 | 
						||
| 
								 | 
							
								                # For this reason, despite having no master node, we need
							 | 
						||
| 
								 | 
							
								                # to check whether the tree variable already exists.
							 | 
						||
| 
								 | 
							
								                target_tree_has_variable = False
							 | 
						||
| 
								 | 
							
								                for item in lst:
							 | 
						||
| 
								 | 
							
								                    if item.name == self.lnx_logic_id:
							 | 
						||
| 
								 | 
							
								                        target_tree_has_variable = True
							 | 
						||
| 
								 | 
							
								                        break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                if not target_tree_has_variable:
							 | 
						||
| 
								 | 
							
								                    var_item = lst.add()
							 | 
						||
| 
								 | 
							
								                    var_item['_name'] = lnx.utils.unique_name_in_lists(
							 | 
						||
| 
								 | 
							
								                        item_lists=[lst], name_attr='name', wanted_name=self.lnx_logic_id, ignore_item=var_item
							 | 
						||
| 
								 | 
							
								                    )
							 | 
						||
| 
								 | 
							
								                    var_item.node_type = self.bl_idname
							 | 
						||
| 
								 | 
							
								                    var_item.color = lnx.utils.get_random_color_rgb()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    target_tree.lnx_treevariableslist_index = len(lst) - 1
							 | 
						||
| 
								 | 
							
								                    lnx.make_state.redraw_ui = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                self.is_master_node = True
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                # Use existing variable
							 | 
						||
| 
								 | 
							
								                for item in lst:
							 | 
						||
| 
								 | 
							
								                    if item.name == self.lnx_logic_id:
							 | 
						||
| 
								 | 
							
								                        self.color = item.color
							 | 
						||
| 
								 | 
							
								                        break
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        super().copy(src_node)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_socket_state_change(self):
							 | 
						||
| 
								 | 
							
								        if self.is_master_node:
							 | 
						||
| 
								 | 
							
								            self._synchronize_to_replicas(self)
							 | 
						||
| 
								 | 
							
								        super().on_socket_state_change()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_logic_id_change(self):
							 | 
						||
| 
								 | 
							
								        tree = self.get_tree()
							 | 
						||
| 
								 | 
							
								        is_linked = self.lnx_logic_id != ''
							 | 
						||
| 
								 | 
							
								        for inp in self.inputs:
							 | 
						||
| 
								 | 
							
								            if is_linked:
							 | 
						||
| 
								 | 
							
								                for link in inp.links:
							 | 
						||
| 
								 | 
							
								                    tree.links.remove(link)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            inp.hide = is_linked
							 | 
						||
| 
								 | 
							
								            inp.enabled = not is_linked  # Hide in sidebar, see Blender's space_node.py
							 | 
						||
| 
								 | 
							
								        super().on_logic_id_change()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_prop_update(self, context: bpy.types.Context, prop_name: str):
							 | 
						||
| 
								 | 
							
								        if self.is_master_node:
							 | 
						||
| 
								 | 
							
								            self._synchronize_to_replicas(self)
							 | 
						||
| 
								 | 
							
								        super().on_prop_update(context, prop_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def on_socket_val_update(self, context: bpy.types.Context, socket: bpy.types.NodeSocket):
							 | 
						||
| 
								 | 
							
								        if self.is_master_node:
							 | 
						||
| 
								 | 
							
								            self._synchronize_to_replicas(self)
							 | 
						||
| 
								 | 
							
								        super().on_socket_val_update(context, socket)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def draw_content(self, context, layout):
							 | 
						||
| 
								 | 
							
								        """Override in variable nodes as replacement for draw_buttons()"""
							 | 
						||
| 
								 | 
							
								        pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @final
							 | 
						||
| 
								 | 
							
								    def draw_buttons(self, context, layout):
							 | 
						||
| 
								 | 
							
								        if self.lnx_logic_id == '':
							 | 
						||
| 
								 | 
							
								            self.draw_content(context, layout)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            txt_wrapper = self.__class__._text_wrapper
							 | 
						||
| 
								 | 
							
								            # Roughly estimate how much text fits in the node's width
							 | 
						||
| 
								 | 
							
								            txt_wrapper.width = self.width / 6
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            msg = f'Value linked to tree variable "{self.lnx_logic_id}"'
							 | 
						||
| 
								 | 
							
								            lines = txt_wrapper.wrap(msg)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            for line in lines:
							 | 
						||
| 
								 | 
							
								                row = layout.row(align=True)
							 | 
						||
| 
								 | 
							
								                row.alignment = 'EXPAND'
							 | 
						||
| 
								 | 
							
								                row.label(text=line)
							 | 
						||
| 
								 | 
							
								                row.scale_y = 0.4
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def draw_label(self) -> str:
							 | 
						||
| 
								 | 
							
								        if self.lnx_logic_id == '':
							 | 
						||
| 
								 | 
							
								            return self.bl_label
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return f'TV: {self.lnx_logic_id}'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def synchronize(cls, tree: bpy.types.NodeTree, logic_id: str):
							 | 
						||
| 
								 | 
							
								        """Synchronizes the value of the master node of the given
							 | 
						||
| 
								 | 
							
								        `logic_id` to all replica nodes.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        master_node = cls.get_master_node(tree, logic_id)
							 | 
						||
| 
								 | 
							
								        master_node._synchronize_to_replicas(master_node)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @staticmethod
							 | 
						||
| 
								 | 
							
								    def choose_new_master_node(tree: bpy.types.NodeTree, logic_id: str) -> bool:
							 | 
						||
| 
								 | 
							
								        """Choose a new master node from the remaining replica nodes.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        Return `True` if a new master node was found, otherwise return
							 | 
						||
| 
								 | 
							
								        `False`.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            node = next(LnxLogicVariableNodeMixin.get_replica_nodes(tree, logic_id))
							 | 
						||
| 
								 | 
							
								        except StopIteration:
							 | 
						||
| 
								 | 
							
								            return False  # No replica node found
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        node.is_master_node = True
							 | 
						||
| 
								 | 
							
								        return True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @staticmethod
							 | 
						||
| 
								 | 
							
								    def get_master_node(tree: bpy.types.NodeTree, logic_id: str) -> Optional['LnxLogicVariableNodeMixin']:
							 | 
						||
| 
								 | 
							
								        for node in tree.nodes:
							 | 
						||
| 
								 | 
							
								            if node.lnx_logic_id == logic_id and isinstance(node, LnxLogicVariableNodeMixin):
							 | 
						||
| 
								 | 
							
								                if node.is_master_node:
							 | 
						||
| 
								 | 
							
								                    return node
							 | 
						||
| 
								 | 
							
								        return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @staticmethod
							 | 
						||
| 
								 | 
							
								    def get_replica_nodes(tree: bpy.types.NodeTree, logic_id: str) -> Generator['LnxLogicVariableNodeMixin', None, None]:
							 | 
						||
| 
								 | 
							
								        """A generator that iterates over all variable nodes for a given
							 | 
						||
| 
								 | 
							
								        ID that are not the master node.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        for node in tree.nodes:
							 | 
						||
| 
								 | 
							
								            if node.lnx_logic_id == logic_id and isinstance(node, LnxLogicVariableNodeMixin):
							 | 
						||
| 
								 | 
							
								                if not node.is_master_node:
							 | 
						||
| 
								 | 
							
								                    yield node
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeAddInputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Add a new input socket to the node set by node_index."""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_add_input'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Add Input'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    socket_type: StringProperty(name='Socket Type', default='LnxDynamicSocket')
							 | 
						||
| 
								 | 
							
								    name_format: StringProperty(name='Name Format', default='Input {0}')
							 | 
						||
| 
								 | 
							
								    index_name_offset: IntProperty(name='Index Name Offset', default=0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        inps = node.inputs
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        socket_types = self.socket_type.split(';')
							 | 
						||
| 
								 | 
							
								        name_formats = self.name_format.split(';')
							 | 
						||
| 
								 | 
							
								        assert len(socket_types) == len(name_formats)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        format_index = (len(inps) + self.index_name_offset) // len(socket_types)
							 | 
						||
| 
								 | 
							
								        for socket_type, name_format in zip(socket_types, name_formats):
							 | 
						||
| 
								 | 
							
								            inp = inps.new(socket_type, name_format.format(str(format_index)))
							 | 
						||
| 
								 | 
							
								            # Make sure inputs don't show up if the node links to a tree variable
							 | 
						||
| 
								 | 
							
								            inp.hide = node.lnx_logic_id != ''
							 | 
						||
| 
								 | 
							
								            inp.enabled = node.lnx_logic_id == ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Reset to default again for subsequent calls of this operator
							 | 
						||
| 
								 | 
							
								        self.node_index = ''
							 | 
						||
| 
								 | 
							
								        self.socket_type = 'LnxDynamicSocket'
							 | 
						||
| 
								 | 
							
								        self.name_format = 'Input {0}'
							 | 
						||
| 
								 | 
							
								        self.index_name_offset = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeAddInputValueButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Add new input"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_add_input_value'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Add Input'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    socket_type: StringProperty(name='Socket Type', default='LnxDynamicSocket')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        inps = array_nodes[self.node_index].inputs
							 | 
						||
| 
								 | 
							
								        inps.new(self.socket_type, 'Value')
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeRemoveInputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Remove last input"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_remove_input'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Remove Input'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    count: IntProperty(name='Number of inputs to remove', default=1, min=1)
							 | 
						||
| 
								 | 
							
								    min_inputs: IntProperty(name='Number of inputs to keep', default=0, min=0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        inps = node.inputs
							 | 
						||
| 
								 | 
							
								        min_inps = self.min_inputs if not hasattr(node, 'min_inputs') else node.min_inputs
							 | 
						||
| 
								 | 
							
								        if len(inps) >= min_inps + self.count:
							 | 
						||
| 
								 | 
							
								            for _ in range(self.count):
							 | 
						||
| 
								 | 
							
								                inps.remove(inps.values()[-1])
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeRemoveInputValueButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Remove last input"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_remove_input_value'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Remove Input'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    target_name: StringProperty(name='Name of socket to remove', default='Value')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        inps = node.inputs
							 | 
						||
| 
								 | 
							
								        min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs
							 | 
						||
| 
								 | 
							
								        if len(inps) > min_inps and inps[-1].name == self.target_name:
							 | 
						||
| 
								 | 
							
								            inps.remove(inps.values()[-1])
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeAddOutputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Add a new output socket to the node set by node_index"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_add_output'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Add Output'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    socket_type: StringProperty(name='Socket Type', default='LnxDynamicSocket')
							 | 
						||
| 
								 | 
							
								    name_format: StringProperty(name='Name Format', default='Output {0}')
							 | 
						||
| 
								 | 
							
								    index_name_offset: IntProperty(name='Index Name Offset', default=0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        outs = array_nodes[self.node_index].outputs
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        socket_types = self.socket_type.split(';')
							 | 
						||
| 
								 | 
							
								        name_formats = self.name_format.split(';')
							 | 
						||
| 
								 | 
							
								        assert len(socket_types) == len(name_formats)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        format_index = (len(outs) + self.index_name_offset) // len(socket_types)
							 | 
						||
| 
								 | 
							
								        for socket_type, name_format in zip(socket_types, name_formats):
							 | 
						||
| 
								 | 
							
								            outs.new(socket_type, name_format.format(str(format_index)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Reset to default again for subsequent calls of this operator
							 | 
						||
| 
								 | 
							
								        self.node_index = ''
							 | 
						||
| 
								 | 
							
								        self.socket_type = 'LnxDynamicSocket'
							 | 
						||
| 
								 | 
							
								        self.name_format = 'Output {0}'
							 | 
						||
| 
								 | 
							
								        self.index_name_offset = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeRemoveOutputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Remove last output"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_remove_output'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Remove Output'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    count: IntProperty(name='Number of outputs to remove', default=1, min=1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        outs = node.outputs
							 | 
						||
| 
								 | 
							
								        min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs
							 | 
						||
| 
								 | 
							
								        if len(outs) >= min_outs + self.count:
							 | 
						||
| 
								 | 
							
								            for _ in range(self.count):
							 | 
						||
| 
								 | 
							
								                outs.remove(outs.values()[-1])
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeAddInputOutputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Add new input and output"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_add_input_output'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Add Input Output'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    in_socket_type: StringProperty(name='In Socket Type', default='LnxDynamicSocket')
							 | 
						||
| 
								 | 
							
								    out_socket_type: StringProperty(name='Out Socket Type', default='LnxDynamicSocket')
							 | 
						||
| 
								 | 
							
								    in_name_format: StringProperty(name='In Name Format', default='Input {0}')
							 | 
						||
| 
								 | 
							
								    out_name_format: StringProperty(name='Out Name Format', default='Output {0}')
							 | 
						||
| 
								 | 
							
								    in_index_name_offset: IntProperty(name='In Name Offset', default=0)
							 | 
						||
| 
								 | 
							
								    out_index_name_offset: IntProperty(name='Out Name Offset', default=0)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        inps = node.inputs
							 | 
						||
| 
								 | 
							
								        outs = node.outputs
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        in_socket_types = self.in_socket_type.split(';')
							 | 
						||
| 
								 | 
							
								        in_name_formats = self.in_name_format.split(';')
							 | 
						||
| 
								 | 
							
								        assert len(in_socket_types) == len(in_name_formats)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        out_socket_types = self.out_socket_type.split(';')
							 | 
						||
| 
								 | 
							
								        out_name_formats = self.out_name_format.split(';')
							 | 
						||
| 
								 | 
							
								        assert len(out_socket_types) == len(out_name_formats)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        in_format_index = (len(outs) + self.in_index_name_offset) // len(in_socket_types)
							 | 
						||
| 
								 | 
							
								        out_format_index = (len(outs) + self.out_index_name_offset) // len(out_socket_types)
							 | 
						||
| 
								 | 
							
								        for socket_type, name_format in zip(in_socket_types, in_name_formats):
							 | 
						||
| 
								 | 
							
								            inps.new(socket_type, name_format.format(str(in_format_index)))
							 | 
						||
| 
								 | 
							
								        for socket_type, name_format in zip(out_socket_types, out_name_formats):
							 | 
						||
| 
								 | 
							
								            outs.new(socket_type, name_format.format(str(out_format_index)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Reset to default again for subsequent calls of this operator
							 | 
						||
| 
								 | 
							
								        self.node_index = ''
							 | 
						||
| 
								 | 
							
								        self.in_socket_type = 'LnxDynamicSocket'
							 | 
						||
| 
								 | 
							
								        self.out_socket_type = 'LnxDynamicSocket'
							 | 
						||
| 
								 | 
							
								        self.in_name_format = 'Input {0}'
							 | 
						||
| 
								 | 
							
								        self.out_name_format = 'Output {0}'
							 | 
						||
| 
								 | 
							
								        self.in_index_name_offset = 0
							 | 
						||
| 
								 | 
							
								        self.out_index_name_offset = 0
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeRemoveInputOutputButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Remove last input and output"""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_remove_input_output'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Remove Input Output'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    in_count: IntProperty(name='Number of inputs to remove', default=1, min=1)
							 | 
						||
| 
								 | 
							
								    out_count: IntProperty(name='Number of inputs to remove', default=1, min=1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        global array_nodes
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        inps = node.inputs
							 | 
						||
| 
								 | 
							
								        outs = node.outputs
							 | 
						||
| 
								 | 
							
								        min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs
							 | 
						||
| 
								 | 
							
								        min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs
							 | 
						||
| 
								 | 
							
								        if len(inps) >= min_inps + self.in_count:
							 | 
						||
| 
								 | 
							
								            for _ in range(self.in_count):
							 | 
						||
| 
								 | 
							
								                inps.remove(inps.values()[-1])
							 | 
						||
| 
								 | 
							
								        if len(outs) >= min_outs + self.out_count:
							 | 
						||
| 
								 | 
							
								            for _ in range(self.out_count):
							 | 
						||
| 
								 | 
							
								                outs.remove(outs.values()[-1])
							 | 
						||
| 
								 | 
							
								        return{'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeCallFuncButton(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    """Operator that calls a function on a specified
							 | 
						||
| 
								 | 
							
								    node (used for dynamic callbacks)."""
							 | 
						||
| 
								 | 
							
								    bl_idname = 'lnx.node_call_func'
							 | 
						||
| 
								 | 
							
								    bl_label = 'Execute'
							 | 
						||
| 
								 | 
							
								    bl_options = {'UNDO', 'INTERNAL'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								    callback_name: StringProperty(name='Callback Name', default='')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        if hasattr(node, self.callback_name):
							 | 
						||
| 
								 | 
							
								            getattr(node, self.callback_name)()
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return {'CANCELLED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Reset to default again for subsequent calls of this operator
							 | 
						||
| 
								 | 
							
								        self.node_index = ''
							 | 
						||
| 
								 | 
							
								        self.callback_name = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return {'FINISHED'}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeSearch(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    bl_idname = "lnx.node_search"
							 | 
						||
| 
								 | 
							
								    bl_label = "Search..."
							 | 
						||
| 
								 | 
							
								    bl_options = {"REGISTER", "INTERNAL"}
							 | 
						||
| 
								 | 
							
								    bl_property = "item"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_search_items(self, context):
							 | 
						||
| 
								 | 
							
								        items = []
							 | 
						||
| 
								 | 
							
								        for node in get_all_nodes():
							 | 
						||
| 
								 | 
							
								            items.append((node.nodetype, node.label, ""))
							 | 
						||
| 
								 | 
							
								        return items
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    item: EnumProperty(items=get_search_items)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def poll(cls, context):
							 | 
						||
| 
								 | 
							
								        return context.space_data.tree_type == 'LnxLogicTreeType' and context.space_data.edit_tree
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @classmethod
							 | 
						||
| 
								 | 
							
								    def description(cls, context, properties):
							 | 
						||
| 
								 | 
							
								        if cls.poll(context):
							 | 
						||
| 
								 | 
							
								            return "Search for a logic node"
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            return "Search for a logic node. This operator is not available" \
							 | 
						||
| 
								 | 
							
								                   " without an active node tree"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def invoke(self, context, event):
							 | 
						||
| 
								 | 
							
								        context.window_manager.invoke_search_popup(self)
							 | 
						||
| 
								 | 
							
								        return {"CANCELLED"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def execute(self, context):
							 | 
						||
| 
								 | 
							
								        """Called when a node is added."""
							 | 
						||
| 
								 | 
							
								        bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.item, use_transform=True)
							 | 
						||
| 
								 | 
							
								        return {"FINISHED"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class BlendSpaceOperator(bpy.types.Operator):
							 | 
						||
| 
								 | 
							
								    bl_idname = "lnx.blend_space_operator"
							 | 
						||
| 
								 | 
							
								    bl_label = "Blend Space Op"
							 | 
						||
| 
								 | 
							
								    bl_description = ""
							 | 
						||
| 
								 | 
							
								    bl_options = {"REGISTER"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    callback: StringProperty(default = "")
							 | 
						||
| 
								 | 
							
								    node_index: StringProperty(name='Node Index', default='')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def invoke(self, context, event):
							 | 
						||
| 
								 | 
							
								        self.node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        self.window = context.window
							 | 
						||
| 
								 | 
							
								        self.node.property2 = True
							 | 
						||
| 
								 | 
							
								        context.window_manager.modal_handler_add(self)
							 | 
						||
| 
								 | 
							
								        return {"RUNNING_MODAL"}
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def convert_back(self, px, py):
							 | 
						||
| 
								 | 
							
								        gui_bounds = self.node.gui_bounds
							 | 
						||
| 
								 | 
							
								        x1 = gui_bounds[0]
							 | 
						||
| 
								 | 
							
								        y1 = gui_bounds[1]
							 | 
						||
| 
								 | 
							
								        width = gui_bounds[2]
							 | 
						||
| 
								 | 
							
								        localX = (px - x1)/width
							 | 
						||
| 
								 | 
							
								        localY = (py - y1 + width)/width
							 | 
						||
| 
								 | 
							
								        return localX, localY
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def convert_forward(self, locX, locY):
							 | 
						||
| 
								 | 
							
								        gui_bounds = self.node.gui_bounds
							 | 
						||
| 
								 | 
							
								        x1 = gui_bounds[0]
							 | 
						||
| 
								 | 
							
								        y1 = gui_bounds[1]
							 | 
						||
| 
								 | 
							
								        width = gui_bounds[2]
							 | 
						||
| 
								 | 
							
								        px = locX * width + x1
							 | 
						||
| 
								 | 
							
								        py = locY * width - width + y1
							 | 
						||
| 
								 | 
							
								        return px, py
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def set_modal(self):
							 | 
						||
| 
								 | 
							
								        self.modal_start = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def unset_modal(self):
							 | 
						||
| 
								 | 
							
								        self.modal_start = False
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def get_modal_running(self):
							 | 
						||
| 
								 | 
							
								        if not hasattr(self, "modal_start"):
							 | 
						||
| 
								 | 
							
								            self.unset_modal()
							 | 
						||
| 
								 | 
							
								        return self.modal_start
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def get_active_point(self, locX, locY):
							 | 
						||
| 
								 | 
							
								        points = self.node.property0
							 | 
						||
| 
								 | 
							
								        visible = self.node.property1
							 | 
						||
| 
								 | 
							
								        for i in range((len(points) // 2) - 1, -1, -1):
							 | 
						||
| 
								 | 
							
								            
							 | 
						||
| 
								 | 
							
								            if(visible[i]):
							 | 
						||
| 
								 | 
							
								                px = points[i * 2]
							 | 
						||
| 
								 | 
							
								                py = points[i * 2 + 1]
							 | 
						||
| 
								 | 
							
								                dist = math.sqrt((locX - px) * (locX - px) + (locY - py) * (locY - py))
							 | 
						||
| 
								 | 
							
								                if(dist < self.node.point_size):
							 | 
						||
| 
								 | 
							
								                    return i
							 | 
						||
| 
								 | 
							
								        return -1
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def get_cursor_in_region(self, locX, locY):
							 | 
						||
| 
								 | 
							
								        point_size = self.node.point_size
							 | 
						||
| 
								 | 
							
								        if (0 - point_size < locX < 1 + point_size) and (0 - point_size < locY < 1 + point_size):
							 | 
						||
| 
								 | 
							
								            return True
							 | 
						||
| 
								 | 
							
								        return False
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    def set_point_coord(self, index, x, y):
							 | 
						||
| 
								 | 
							
								        self.node.property0[index * 2] = x
							 | 
						||
| 
								 | 
							
								        self.node.property0[index * 2 + 1] = y
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def modal(self, context, event):
							 | 
						||
| 
								 | 
							
								        self.node = array_nodes[self.node_index]
							 | 
						||
| 
								 | 
							
								        self.mousePosition = (event.mouse_region_x, event.mouse_region_y)
							 | 
						||
| 
								 | 
							
								        if(context.area is None or context.area.type != "NODE_EDITOR"):
							 | 
						||
| 
								 | 
							
								            return{"PASS_THROUGH"}
							 | 
						||
| 
								 | 
							
								        context.area.tag_redraw()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if not self.node.property2 or not self.node.advanced_draw_run:
							 | 
						||
| 
								 | 
							
								            self.node.property2 = False
							 | 
						||
| 
								 | 
							
								            return {"FINISHED"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if event.type == "LEFTMOUSE":
							 | 
						||
| 
								 | 
							
								            if event.value == "PRESS":
							 | 
						||
| 
								 | 
							
								                region = context.region.view2d
							 | 
						||
| 
								 | 
							
								                x, y = region.region_to_view(event.mouse_region_x, event.mouse_region_y)
							 | 
						||
| 
								 | 
							
								                locX, locY = self.convert_back(x, y)
							 | 
						||
| 
								 | 
							
								                if self.get_cursor_in_region(locX, locY):
							 | 
						||
| 
								 | 
							
								                    self.set_modal()
							 | 
						||
| 
								 | 
							
								                    self.node.active_point_index = self.get_active_point(locX, locY)
							 | 
						||
| 
								 | 
							
								                    if self.node.active_point_index != -1:
							 | 
						||
| 
								 | 
							
								                        self.node.active_point_index_ref = self.node.active_point_index            
							 | 
						||
| 
								 | 
							
								            if event.value == "RELEASE":
							 | 
						||
| 
								 | 
							
								                self.node.active_point_index = -1
							 | 
						||
| 
								 | 
							
								                if self.get_modal_running():
							 | 
						||
| 
								 | 
							
								                    context.window.cursor_modal_restore()
							 | 
						||
| 
								 | 
							
								                    self.unset_modal()
							 | 
						||
| 
								 | 
							
								                
							 | 
						||
| 
								 | 
							
								        if event.type == "MOUSEMOVE":
							 | 
						||
| 
								 | 
							
								            active_point = self.node.active_point_index
							 | 
						||
| 
								 | 
							
								            if active_point != -1:
							 | 
						||
| 
								 | 
							
								                context.window.cursor_modal_set("SCROLL_XY")
							 | 
						||
| 
								 | 
							
								                region = context.region.view2d
							 | 
						||
| 
								 | 
							
								                x, y = region.region_to_view(event.mouse_region_x, event.mouse_region_y)
							 | 
						||
| 
								 | 
							
								                locX, locY = self.convert_back(x, y)
							 | 
						||
| 
								 | 
							
								                if self.get_cursor_in_region(locX, locY):
							 | 
						||
| 
								 | 
							
								                    if(active_point < 10):
							 | 
						||
| 
								 | 
							
								                        newX = round(locX * 20) * 0.05
							 | 
						||
| 
								 | 
							
								                        newY = round(locY * 20) * 0.05
							 | 
						||
| 
								 | 
							
								                        self.set_point_coord(active_point, newX, newY)
							 | 
						||
| 
								 | 
							
								                    else:
							 | 
						||
| 
								 | 
							
								                        self.set_point_coord(active_point, locX, locY)
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    context.window.cursor_warp(event.mouse_prev_x, event.mouse_prev_y)
							 | 
						||
| 
								 | 
							
								        
							 | 
						||
| 
								 | 
							
								        if self.get_modal_running():
							 | 
						||
| 
								 | 
							
								            return{"RUNNING_MODAL"}
							 | 
						||
| 
								 | 
							
								        
							 | 
						||
| 
								 | 
							
								        return{"PASS_THROUGH"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def finish(self):
							 | 
						||
| 
								 | 
							
								        return {"FINISHED"}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class LnxNodeCategory:
							 | 
						||
| 
								 | 
							
								    """Represents a category (=directory) of logic nodes."""
							 | 
						||
| 
								 | 
							
								    def __init__(self, name: str, icon: str, description: str, category_section: str):
							 | 
						||
| 
								 | 
							
								        self.name = name
							 | 
						||
| 
								 | 
							
								        self.icon = icon
							 | 
						||
| 
								 | 
							
								        self.description = description
							 | 
						||
| 
								 | 
							
								        self.category_section = category_section
							 | 
						||
| 
								 | 
							
								        self.node_sections: ODict[str, List[NodeItem]] = OrderedDict()
							 | 
						||
| 
								 | 
							
								        self.deprecated_nodes: List[NodeItem] = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def register_node(self, node_type: Type[bpy.types.Node], node_section: str) -> None:
							 | 
						||
| 
								 | 
							
								        """Registers a node to this category so that it will be
							 | 
						||
| 
								 | 
							
								        displayed int the `Add node` menu."""
							 | 
						||
| 
								 | 
							
								        self.add_node_section(node_section)
							 | 
						||
| 
								 | 
							
								        self.node_sections[node_section].append(lnx.node_utils.nodetype_to_nodeitem(node_type))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def register_deprecated_node(self, node_type: Type[bpy.types.Node]) -> None:
							 | 
						||
| 
								 | 
							
								        if hasattr(node_type, 'lnx_is_obsolete') and node_type.lnx_is_obsolete:
							 | 
						||
| 
								 | 
							
								            self.deprecated_nodes.append(lnx.node_utils.nodetype_to_nodeitem(node_type))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def get_all_nodes(self) -> Generator[NodeItem, None, None]:
							 | 
						||
| 
								 | 
							
								        """Returns all nodes that are registered into this category."""
							 | 
						||
| 
								 | 
							
								        yield from itertools.chain(*self.node_sections.values())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def add_node_section(self, name: str):
							 | 
						||
| 
								 | 
							
								        """Adds a node section to this category."""
							 | 
						||
| 
								 | 
							
								        if name not in self.node_sections:
							 | 
						||
| 
								 | 
							
								            self.node_sections[name] = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def sort_nodes(self):
							 | 
						||
| 
								 | 
							
								        for node_section in self.node_sections:
							 | 
						||
| 
								 | 
							
								            self.node_sections[node_section] = sorted(self.node_sections[node_section], key=lambda item: item.label)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def category_exists(name: str) -> bool:
							 | 
						||
| 
								 | 
							
								    for category_section in category_items:
							 | 
						||
| 
								 | 
							
								        for c in category_items[category_section]:
							 | 
						||
| 
								 | 
							
								            if c.name == name:
							 | 
						||
| 
								 | 
							
								                return True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_category(name: str) -> Optional[LnxNodeCategory]:
							 | 
						||
| 
								 | 
							
								    for category_section in category_items:
							 | 
						||
| 
								 | 
							
								        for c in category_items[category_section]:
							 | 
						||
| 
								 | 
							
								            if c.name == name:
							 | 
						||
| 
								 | 
							
								                return c
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_all_categories() -> Generator[LnxNodeCategory, None, None]:
							 | 
						||
| 
								 | 
							
								    for section_categories in category_items.values():
							 | 
						||
| 
								 | 
							
								        yield from itertools.chain(section_categories)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_all_nodes() -> Generator[NodeItem, None, None]:
							 | 
						||
| 
								 | 
							
								    for category in get_all_categories():
							 | 
						||
| 
								 | 
							
								        yield from itertools.chain(category.get_all_nodes())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_category_section(name: str) -> None:
							 | 
						||
| 
								 | 
							
								    """Adds a section of categories to the node menu to group multiple
							 | 
						||
| 
								 | 
							
								    categories visually together. The given name only acts as an ID and
							 | 
						||
| 
								 | 
							
								    is not displayed in the user inferface."""
							 | 
						||
| 
								 | 
							
								    global category_items
							 | 
						||
| 
								 | 
							
								    if name not in category_items:
							 | 
						||
| 
								 | 
							
								        category_items[name] = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_node_section(name: str, category: str) -> None:
							 | 
						||
| 
								 | 
							
								    """Adds a section of nodes to the sub menu of the given category to
							 | 
						||
| 
								 | 
							
								    group multiple nodes visually together. The given name only acts as
							 | 
						||
| 
								 | 
							
								    an ID and is not displayed in the user inferface."""
							 | 
						||
| 
								 | 
							
								    node_category = get_category(category)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if node_category is not None:
							 | 
						||
| 
								 | 
							
								        node_category.add_node_section(name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_category(category: str, section: str = 'default', icon: str = 'BLANK1', description: str = '') -> Optional[LnxNodeCategory]:
							 | 
						||
| 
								 | 
							
								    """Adds a category of nodes to the node menu."""
							 | 
						||
| 
								 | 
							
								    global category_items
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    add_category_section(section)
							 | 
						||
| 
								 | 
							
								    if not category_exists(category):
							 | 
						||
| 
								 | 
							
								        node_category = LnxNodeCategory(category, icon, description, section)
							 | 
						||
| 
								 | 
							
								        category_items[section].append(node_category)
							 | 
						||
| 
								 | 
							
								        return node_category
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return None
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'default', is_obsolete: bool = False) -> None:
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    Registers a node to the given category. If no section is given, the
							 | 
						||
| 
								 | 
							
								    node is put into the default section that does always exist.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Warning: Make sure that this function is not called multiple times per node!
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    global nodes
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    category = eval_node_category(node_type, category)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    nodes.append(node_type)
							 | 
						||
| 
								 | 
							
								    node_category = get_category(category)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if node_category is None:
							 | 
						||
| 
								 | 
							
								        node_category = add_category(category)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if is_obsolete:
							 | 
						||
| 
								 | 
							
								        # We need the obsolete nodes to be registered in order to have them replaced,
							 | 
						||
| 
								 | 
							
								        # but do not add them to the menu.
							 | 
						||
| 
								 | 
							
								        if node_category is not None:
							 | 
						||
| 
								 | 
							
								            # Make the deprecated nodes available for documentation purposes
							 | 
						||
| 
								 | 
							
								            node_category.register_deprecated_node(node_type)
							 | 
						||
| 
								 | 
							
								        return
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    node_category.register_node(node_type, section)
							 | 
						||
| 
								 | 
							
								    node_type.bl_icon = node_category.icon
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def eval_node_category(node: Union[LnxLogicTreeNode, Type[LnxLogicTreeNode]], category='') -> str:
							 | 
						||
| 
								 | 
							
								    """Return the effective category name, that is the category name of
							 | 
						||
| 
								 | 
							
								    the given node with resolved `PKG_AS_CATEGORY`.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								    if category == '':
							 | 
						||
| 
								 | 
							
								        category = node.lnx_category
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if category == PKG_AS_CATEGORY:
							 | 
						||
| 
								 | 
							
								        return node.__module__.rsplit('.', 2)[-2].capitalize()
							 | 
						||
| 
								 | 
							
								    return category
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def deprecated(*alternatives: str, message=""):
							 | 
						||
| 
								 | 
							
								    """Class decorator to deprecate logic node classes. You can pass multiple string
							 | 
						||
| 
								 | 
							
								    arguments with the names of the available alternatives as well as a message
							 | 
						||
| 
								 | 
							
								    (keyword-param only) with further information about the deprecation."""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def wrapper(cls: LnxLogicTreeNode) -> LnxLogicTreeNode:
							 | 
						||
| 
								 | 
							
								        cls.bl_label += ' (Deprecated)'
							 | 
						||
| 
								 | 
							
								        if hasattr(cls, 'bl_description'):
							 | 
						||
| 
								 | 
							
								            cls.bl_description = f'Deprecated. {cls.bl_description}'
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            cls.bl_description = 'Deprecated.'
							 | 
						||
| 
								 | 
							
								        cls.bl_icon = 'ERROR'
							 | 
						||
| 
								 | 
							
								        cls.lnx_is_obsolete = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Deprecated nodes must use a category other than PKG_AS_CATEGORY
							 | 
						||
| 
								 | 
							
								        # in order to prevent an empty 'Deprecated' category showing up
							 | 
						||
| 
								 | 
							
								        # in the add node menu and in the generated wiki pages. The
							 | 
						||
| 
								 | 
							
								        # "old" category is still used to put the node into the correct
							 | 
						||
| 
								 | 
							
								        # category in the wiki.
							 | 
						||
| 
								 | 
							
								        assert cls.lnx_category != PKG_AS_CATEGORY, f'Deprecated node {cls.__name__} is missing an explicit category definition!'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if cls.__doc__ is None:
							 | 
						||
| 
								 | 
							
								            cls.__doc__ = ''
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if len(alternatives) > 0:
							 | 
						||
| 
								 | 
							
								            cls.__doc__ += '\n' + f'@deprecated {",".join(alternatives)}: {message}'
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            cls.__doc__ += '\n' + f'@deprecated : {message}'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return cls
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return wrapper
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def is_logic_node_context(context: bpy.context) -> bool:
							 | 
						||
| 
								 | 
							
								    """Return whether the given bpy context is inside a logic node editor."""
							 | 
						||
| 
								 | 
							
								    return context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'LnxLogicTreeType'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def is_logic_node_edit_context(context: bpy.context) -> bool:
							 | 
						||
| 
								 | 
							
								    """Return whether the given bpy context is inside a logic node editor and tree is being edited."""
							 | 
						||
| 
								 | 
							
								    if context.space_data.type == 'NODE_EDITOR' and context.space_data.tree_type == 'LnxLogicTreeType':
							 | 
						||
| 
								 | 
							
								        return context.space_data.edit_tree
							 | 
						||
| 
								 | 
							
								    return False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def reset_globals():
							 | 
						||
| 
								 | 
							
								    global nodes
							 | 
						||
| 
								 | 
							
								    global category_items
							 | 
						||
| 
								 | 
							
								    nodes = []
							 | 
						||
| 
								 | 
							
								    category_items = OrderedDict()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								__REG_CLASSES = (
							 | 
						||
| 
								 | 
							
								    LnxNodeSearch,
							 | 
						||
| 
								 | 
							
								    LnxNodeAddInputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeAddInputValueButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeRemoveInputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeRemoveInputValueButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeAddOutputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeRemoveOutputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeAddInputOutputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeRemoveInputOutputButton,
							 | 
						||
| 
								 | 
							
								    LnxNodeCallFuncButton,
							 | 
						||
| 
								 | 
							
								    BlendSpaceOperator
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								register, unregister = bpy.utils.register_classes_factory(__REG_CLASSES)
							 |