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