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) |