forked from LeenkxTeam/LNXSDK
		
	
		
			
				
	
	
		
			1170 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1170 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections import OrderedDict
 | 
						|
import itertools
 | 
						|
import math
 | 
						|
import textwrap
 | 
						|
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
 | 
						|
from typing import OrderedDict as ODict  # Prevent naming conflicts
 | 
						|
 | 
						|
try:
 | 
						|
    from typing import final
 | 
						|
except ImportError:
 | 
						|
    # Python < 3.8 compatibility
 | 
						|
    def final(f):
 | 
						|
        """No final in Python < 3.8"""
 | 
						|
        return f
 | 
						|
 | 
						|
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)
 |