]> git.walde.dev - blender_node_tools/commitdiff
Init commit
authorDustin Walde <redacted>
Sat, 10 Apr 2021 21:55:27 +0000 (14:55 -0700)
committerDustin Walde <redacted>
Sat, 10 Apr 2021 21:55:27 +0000 (14:55 -0700)
- Implemented primary functions

  - get_node
  - link_nodes
  - remove_links
  - resolve_path
  - build_path

- Began documentation

README.rst [new file with mode: 0644]
blender_node_tools.py [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..ea34ca6
--- /dev/null
@@ -0,0 +1,51 @@
+blender_node_tools
+==================
+
+A python module of functions to aid in manipulating node connections
+in Blender.
+
+The goal is to make it simpler when adding / removing links between
+nodes of a NodeTree. Using the Blender Python API, adding links looks
+something like::
+
+    node_tree.links.new(
+        node_tree.nodes['From Node'].outputs['Output'],
+        node_tree.nodes['To Node'].inputs['Input']
+        )
+
+Now this can look like::
+
+    link_nodes(node_tree, 'From Node', 'To Node:Input')
+
+Even more roundabout was the removal of links:
+
+    for link in node_tree.links:
+        if link.to_node == node_tree.nodes['Node'] \
+            or link.from_node == node_tree.nodes['Node']:
+            node_tree.links.remove(link)
+
+Now::
+
+    remove_links(node_tree, 'Node')
+
+Or to remove all inputs::
+
+    remove_links(node_tree, '+Node')
+
+Outputs::
+
+    remove_links(node_tree, '-Node')
+
+
+Something like a compositor mix node have two inputs of the same name
+"Image". These may be targeting with a subscript index notation::
+
+    link_nodes(node_tree, "Image Texture", "Mix:Image[0]")
+    link_nodes(node_tree, "Fade Texture",  "Mix:Image[1]")
+
+Requirements
+------------
+
+Python version 3.6 or later.
+Tested with Blender version 2.92, will likely work for previous
+versions.
\ No newline at end of file
diff --git a/blender_node_tools.py b/blender_node_tools.py
new file mode 100644 (file)
index 0000000..529b881
--- /dev/null
@@ -0,0 +1,286 @@
+# ©2021 Dustin Walde
+# Licensed under GPL-3.0
+
+import sys
+
+from typing import (
+    Tuple,
+    List,
+    Optional,
+    Union,
+    )
+
+# compatability shim
+if sys.version_info.minor < 8:
+    def get_origin(item):
+        return item.__origin__
+
+    def get_args(item):
+        return item.__args__
+else:
+    from typing import get_args, get_origin
+
+from bpy.types import (
+    CompositorNodeGroup,
+    CompositorNodeCustomGroup,
+    GeometryNodeGroup,
+    Node,
+    NodeCustomGroup,
+    NodeGroup,
+    NodeInputs,
+    NodeLink,
+    NodeOutputs,
+    NodeSocket,
+    NodeTree,
+    ShaderNodeGroup,
+    ShaderNodeCustomGroup,
+    TextureNodeGroup,
+    )
+
+NodeGroupType = Union[
+    CompositorNodeGroup,
+    CompositorNodeCustomGroup,
+    GeometryNodeGroup,
+    NodeCustomGroup,
+    NodeGroup,
+    ShaderNodeGroup,
+    ShaderNodeCustomGroup,
+    TextureNodeGroup,
+    ] # all have a #node_tree property and are Nodes
+NodeType = Union[str, Node, NodeSocket]
+
+def is_group(node: Node) -> bool:
+    """
+    Check if node is a *GroupNode of some sort
+    """
+    return _is_union_instance(node, NodeGroupType)
+
+def build_path(*nodes: str,
+    input: Union[bool,str]=False,
+    output: Union[bool,str]=False) \
+        -> str:
+    if input != False and output != False:
+        raise ValueError("Path cannot be both input and output")
+
+    node_path = '/'.join(nodes)
+
+    if type(input) == str:
+        return '+{}:{}'.format(node_path, input)
+    elif input:
+        return '+{}'.format(node_path)
+
+    if type(output) == str:
+        return '-{}:{}'.format(node_path, output)
+    elif input:
+        return '-{}'.format(node_path)
+
+    return node_path
+
+def link_nodes(
+    tree: NodeTree,
+    link_from: NodeType,
+    link_to: NodeType,
+    preserve_existing: bool = False) \
+        -> int:
+    """
+    Link from output node to input node.
+    If no sockets are specified, this will match as many as it can find.
+    Will not create new links to input sockets if preserve_existing is
+    set.
+    """
+    connections = 0
+
+    output_sockets = []
+    input_sockets = []
+
+    parent_tree = tree
+
+    if isinstance(link_from, NodeSocket):
+        if link_from.is_output:
+            output_sockets.append(link_from)
+    elif isinstance(link_from, Node):
+        for output in link_from.outputs:
+            output_sockets.append(output)
+    else:
+        parent_tree, _, _, output_sockets = resolve_path(tree, link_from)
+
+    if isinstance(link_to, NodeSocket):
+        if not link_to.is_output:
+            input_sockets.append(link_to)
+    elif isinstance(link_to, Node):
+        for input in link_to.inputs:
+            input_sockets.append(input)
+    else:
+        parent_tree, _, input_sockets, _ = resolve_path(tree, link_to)
+
+    for i in range(len(input_sockets)):
+        input_sockets[i] = [
+            input_sockets[i],
+            preserve_existing and len(input_sockets[i].links) > 0]
+
+    for output in output_sockets:
+        for input_data in input_sockets:
+            input, matched = input_data
+            if matched:
+                continue
+
+            if output.type == input.type \
+                or (len(output_sockets) == 1 and len(input_sockets) == 1):
+                parent_tree.links.new(output, input)
+                input_data[1] = True
+                connections += 1
+                break
+
+    return connections
+
+def get_link(tree: NodeTree, of: NodeType) -> Optional[NodeLink]:
+    links = get_links(tree, of)
+    if len(links) == 0:
+        return None
+    return links[0]
+
+def get_links(tree: NodeTree, of: NodeType) -> List[NodeLink]:
+    links = []
+    if isinstance(of, NodeSocket):
+        links = of.links
+    elif isinstance(of, Node):
+        for input in of.inputs:
+            links.extend(input.links)
+        for output in of.outputs:
+            links.extend(output.links)
+    else:
+        _, _, inputs, outputs = resolve_path(tree, of)
+        for input in inputs:
+            links.extend(input.links)
+        for output in outputs:
+            links.extend(output.links)
+
+    return links
+
+def remove_links(tree: NodeTree, node: NodeType) -> int:
+    removed = 0
+    if isinstance(node, str):
+        node_tree, node, inputs, outputs = resolve_path(tree, node)
+        for input in inputs:
+            for link in input.links:
+                node_tree.links.remove(link)
+                removed += 1
+        for output in outputs:
+            for link in output.links:
+                node_tree.links.remove(link)
+                removed += 1
+    else:
+        for link in get_links(tree, node):
+            tree.links.remove(link)
+            removed += 1
+
+    return removed
+
+def get_socket(tree: NodeTree, node_path: str) -> NodeSocket:
+    return get_sockets(tree, node_path)[0]
+
+def get_sockets(tree: NodeTree, node_path: str) -> List[NodeSocket]:
+    _, _, inputs, outputs = resolve_path(tree, node_path)
+    inputs.extend(outputs)
+    return inputs
+
+def get_tree(tree: NodeTree, node_path: str) -> NodeTree:
+    return resolve_path(tree, node_path)[0]
+
+def get_node(tree: NodeTree, node_path: str) -> Node:
+    """
+    Retrieve the the Node from tree at the given node_path.
+
+    raises ValueError if node_path is invalid.
+    """
+    return resolve_path(tree, node_path)[1]
+
+def resolve_path(tree: NodeTree, node_path: str) \
+    -> Tuple[NodeTree, Node, List[NodeSocket], List[NodeSocket]]:
+
+    path_parts, socket_name, socket_index, include_inputs, include_outputs = \
+        _parse_path(node_path)
+
+    current_tree = tree
+    for node in path_parts[:-1]:
+        if node not in current_tree.nodes:
+            _invalid_path(node_path,
+                "Node name ({}) not found".format(node))
+
+        current_tree = current_tree.nodes[node]
+
+        if not is_group(current_tree):
+            _invalid_path(node_path,
+                "Parent node ({}) is not a group.".format(node))
+
+        current_tree = current_tree.node_tree
+
+    leaf_node = path_parts[-1]
+
+    if leaf_node not in current_tree.nodes:
+        _invalid_path(node_path,
+            "Node name ({}) not found".format(leaf_node))
+
+    node = current_tree.nodes[leaf_node]
+
+    inputs = []
+    outputs = []
+
+    def _append_matching_sockets(
+        include: bool,
+        node_sockets: Union[NodeInputs,NodeOutputs],
+        out_sockets: List[NodeSocket]):
+        name_count = 0
+        if include:
+            for socket in node_sockets:
+                if socket_name is None or socket_name == socket.name:
+                    if socket_index < 0 or socket_index == name_count:
+                        out_sockets.append(socket)
+                    name_count += 1
+
+    _append_matching_sockets(include_inputs, node.inputs, inputs)
+    _append_matching_sockets(include_outputs, node.outputs, outputs)
+
+    return (current_tree, node, inputs, outputs)
+
+# helpers ↓
+
+def _invalid_path(node_path, message):
+    raise ValueError("Invalid Path: {}\n{}".format(node_path, message))
+
+def _is_union_instance(item, union_type) -> bool:
+    if not get_origin(union_type) is Union:
+        raise TypeError("Expected Union type")
+
+    return isinstance(item, get_args(union_type))
+
+def _parse_path(node_path: str) -> Tuple[List[str], str, int, bool, bool]:
+    input = True
+    output = True
+    socket = None
+    socket_index = -1
+    path_parts = None
+
+    if node_path[0] == '+':
+        output = False
+        node_path = node_path[1:]
+    elif node_path[0] == '-':
+        input = False
+        node_path = node_path[1:]
+
+    parts = node_path.split(':')
+    if len(parts) > 1:
+        socket = parts[-1]
+        path_parts = ':'.join(parts[:-1]).split('/')
+    else:
+        path_parts = node_path.split('/')
+
+    if socket is not None:
+        left_bracket = socket.find('[')
+        right_bracket = socket.find(']')
+
+        if left_bracket >= 0 and right_bracket >= 2:
+            socket_index = int(socket[left_bracket+1:right_bracket])
+            socket = socket[:left_bracket]
+
+    return (path_parts, socket, socket_index, input, output)
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..b25c370
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,15 @@
+from setuptools import setup
+
+with open("README.rst", 'r') as readme:
+    long_description = readme.read()
+
+setup(
+    name='blender_node_tools',
+    version='1.0',
+    description="",
+    license="GPL-3.0",
+    long_description=long_description,
+    author="Dustin Walde",
+    author_email='dustin@arcsineimaging.com',
+    packages=['blender_node_tools'],
+    )
\ No newline at end of file