Loop Nodes #4

Open
Denys Hsu wants to merge 5 commits from cgtinker/powership:loop into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
4 changed files with 80 additions and 50 deletions
Showing only changes of commit 764420af01 - Show all commits

3
gui.py
View File

@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
import bpy import bpy
from typing import List
class RIGNODES_MT_snap_pie(bpy.types.Menu): class RIGNODES_MT_snap_pie(bpy.types.Menu):
@ -60,7 +61,7 @@ def draw_nodetree_header_menu(self, context):
row.operator("rignodes.rebuild_node") row.operator("rignodes.rebuild_node")
addon_keymaps = [] addon_keymaps: List[bpy.types.KeyMap] = []
classes = (RIGNODES_MT_snap_pie,) classes = (RIGNODES_MT_snap_pie,)
_register, _unregister = bpy.utils.register_classes_factory(classes) _register, _unregister = bpy.utils.register_classes_factory(classes)

121
nodes.py
View File

@ -81,7 +81,7 @@ class RigNodesNodeTree(bpy.types.NodeTree):
# Set iteration count foreach (nested) loop. # Set iteration count foreach (nested) loop.
for output_node in output_nodes: for output_node in output_nodes:
input_node = output_node._get_connection_node() input_node = output_node._get_connection_node()
nodes = list(output_node._get_nodes_between(input_node)) nodes = list(output_node._get_loop_nodes(input_node))
for node in nodes: for node in nodes:
cgtinker marked this conversation as resolved

There is no need to construct a list here, just loop over the generator returned by output_node._get_loop_nodes()

There is no need to construct a list here, just loop over the generator returned by `output_node._get_loop_nodes()`
if node == output_node: if node == output_node:
continue continue
@ -93,12 +93,12 @@ class RigNodesNodeTree(bpy.types.NodeTree):
depsgraph: bpy.types.Depsgraph, depsgraph: bpy.types.Depsgraph,
to_visit: deque["AbstractRigNodesNode"], to_visit: deque["AbstractRigNodesNode"],
depth: int = 1, depth: int = 1,
) -> deque['AbstractRigNodesNode']: ) -> deque["AbstractRigNodesNode"]:
"""Run one iteration of a loop. """Runs (nested) loops.
:return: Node(s) to visit next.""" :return: Node(s) to visit next."""
cgtinker marked this conversation as resolved

Formatting: when writing multi-line docstrings, the closing """ should be on a line of its own (see PEP 257)

Formatting: when writing multi-line docstrings, the closing `"""` should be on a line of its own (see [PEP 257](https://peps.python.org/pep-0257/#multi-line-docstrings))
Review

Sorry, still stumbling.

Sorry, still stumbling.
indent = '\t' * depth indent = "\t" * depth
print(f"{indent}\033[38;5;214mRunning {node}\033[0m") print(f"{indent}\033[38;5;214mRunning {node}\033[0m")
input_node = node input_node = node
@ -111,7 +111,7 @@ class RigNodesNodeTree(bpy.types.NodeTree):
if isinstance(node, LoopInputNode) and node != input_node: if isinstance(node, LoopInputNode) and node != input_node:
# Find the nested loop. # Find the nested loop.
nested_output_node = node._get_connection_node() nested_output_node = node._get_connection_node()
nested_loop = deque(nested_output_node._get_nodes_between(node)) nested_loop = deque(nested_output_node._get_loop_nodes(node))
nested_loop.append(nested_output_node) nested_loop.append(nested_output_node)
# Don't revisit nested nodes in the parent loop. # Don't revisit nested nodes in the parent loop.
@ -120,13 +120,12 @@ class RigNodesNodeTree(bpy.types.NodeTree):
# Run the nested loop using the current inputs. # Run the nested loop using the current inputs.
for _ in range(node.num_socks): for _ in range(node.num_socks):
self._run_loop(node, depsgraph, nested_loop.copy(), depth+1) self._run_loop(node, depsgraph, nested_loop.copy(), depth + 1)
continue continue
if node in visited: if node in visited:
# Nested nodes are added to visited nodes as they get executed in advance. # Nested nodes are added to visited nodes as they get executed in advance.
# A Node can alsp be visited when a node has inputs from two different # A Node can also be visited when a node has inputs from two different
# nodes; both of those will list this node as 'successor'. # nodes; both of those will list this node as 'successor'.
continue continue
@ -139,7 +138,6 @@ class RigNodesNodeTree(bpy.types.NodeTree):
return to_visit return to_visit
def _run_from_node( def _run_from_node(
self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractRigNodesNode" self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractRigNodesNode"
) -> None: ) -> None:
@ -160,11 +158,22 @@ class RigNodesNodeTree(bpy.types.NodeTree):
continue continue
if isinstance(node, LoopInputNode): if isinstance(node, LoopInputNode):
# Run all iterations if the found loop (recursive if nested). if node in visited:
# nested_output_node = node._get_connection_node() to_visit.popleft()
# nested_loop = deque(nested_output_node._get_nodes_between(node)) continue
# nested_loop.append(nested_output_node)
self._run_loop(node, depsgraph, to_visit) # Find the loop.
output_node = node._get_connection_node()
loop = deque(output_node._get_loop_nodes(node))
loop.append(output_node)
# Add (nested) loop nodes to visited.
for loop_node in loop:
visited.add(loop_node)
# Run all iterations of the found loop (recursive if nested).
for _ in range(0, output_node.num_socks):
self._run_loop(node, depsgraph, to_visit.copy())
continue continue
# Everything that this node depends on has run, so time to run it # Everything that this node depends on has run, so time to run it
@ -291,15 +300,15 @@ class AbstractRigNodesNode(bpy.types.Node):
def _outputs(self): def _outputs(self):
return self.outputs return self.outputs
def _get_nodes_between( def _get_loop_nodes(
self, to_node: "AbstractRigNodesNode" self: "LoopOutputNode", input_node: "LoopInputNode"
) -> Iterable["AbstractRigNodesNode"]: ) -> Iterable["AbstractRigNodesNode"]:
# TODO: the naming is bad here I think. Check that it's actually from -> to or to -> from """Generator, yields nodes that should be executed before the loop output node."""
nodes = list(self._connected_nodes(self._inputs, "from_socket")) nodes = list(self._connected_nodes(self._inputs, "from_socket"))
if not to_node in nodes and len(nodes) > 0: if not input_node in nodes and len(nodes) > 0:
from_node = nodes[-1] from_node = nodes[-1]
yield from from_node._get_nodes_between(to_node) yield from from_node._get_loop_nodes(input_node)
yield from nodes yield from nodes
def exec_order_prerequisites(self) -> Iterable["AbstractRigNodesNode"]: def exec_order_prerequisites(self) -> Iterable["AbstractRigNodesNode"]:
@ -521,6 +530,20 @@ class AbstractRigNodesNode(bpy.types.Node):
tree.links.new(from_socket, to_socket) tree.links.new(from_socket, to_socket)
class ObjectInputNode(AbstractRigNodesNode):
bl_idname = "ObjectInputNode"
bl_label = "Object Input Node"
bl_icon = "EMPTY_ARROWS"
def init(self, context: bpy.types.Context) -> None:
self.inputs.new("NodeSocketObject", f"Object")
self.outputs.new("NodeSocketObject", f"Object")
def execute(self, depsgraph: bpy.types.Depsgraph) -> None:
obj = self._get_input_value(f"Object", bpy.types.Object)
self.outputs["Object"].default_value = obj
def _on_update_recreate_node(self: "SequenceNode", context: bpy.types.Context) -> None: def _on_update_recreate_node(self: "SequenceNode", context: bpy.types.Context) -> None:
self.recreate(context) self.recreate(context)
@ -559,13 +582,6 @@ class LoopInputNode(AbstractRigNodesNode):
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
layout.prop(self, "dtype") layout.prop(self, "dtype")
layout.prop(self, "num_socks") layout.prop(self, "num_socks")
layout.prop(self, "index")
# 67649: The socket_value_update fn does not work when changing values in a node tree.
# Therefore using update for prototying.
def update(self):
if "Loop Connection" in self.outputs:
setattr(self.outputs["Loop Connection"], "default_value", self.num_socks)
def _get_connection_node(self) -> "LoopOutputNode": def _get_connection_node(self) -> "LoopOutputNode":
# The 'Loop Connection' should be connected to a 'Loop Input Node', # The 'Loop Connection' should be connected to a 'Loop Input Node',
@ -582,6 +598,12 @@ class LoopInputNode(AbstractRigNodesNode):
return connected_socket.node return connected_socket.node
raise RuntimeError("Loop Output Node is required to execute a loop.") raise RuntimeError("Loop Output Node is required to execute a loop.")
# 67649: The socket_value_update fn does not work when changing values in a node tree.
# Therefore using update for prototying.
def update(self):
if "Loop Connection" in self.outputs:
setattr(self.outputs["Loop Connection"], "default_value", self.num_socks)
def reset_run(self): def reset_run(self):
super().reset_run() super().reset_run()
self.index = 0 self.index = 0
@ -657,7 +679,10 @@ class LoopOutputNode(AbstractRigNodesNode):
) -> None: ) -> None:
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
layout.prop(self, "dtype") layout.prop(self, "dtype")
layout.prop(self, "index")
@AbstractRigNodesNode._inputs.getter # type: ignore
def _inputs(self):
return self.inputs[1:]
def _get_connection_node(self) -> "LoopInputNode": def _get_connection_node(self) -> "LoopInputNode":
# The 'Loop Connection' should be connected to a 'Loop Input Node', # The 'Loop Connection' should be connected to a 'Loop Input Node',
@ -672,9 +697,21 @@ class LoopOutputNode(AbstractRigNodesNode):
return connected_socket.node return connected_socket.node
raise RuntimeError("Loop Input Node is required to execute a loop.") raise RuntimeError("Loop Input Node is required to execute a loop.")
@AbstractRigNodesNode._inputs.getter # type: ignore def reset_run(self) -> None:
def _inputs(self): super().reset_run()
return self.inputs[1:] self.index = 0
self.iterations = self.num_socks
# 67649: The socket_value_update fn does not work when changing values in a node tree.
# Therefore using update for prototying the loop function.
# This allows to dynamically change the sockets based loop input socket amount.
def update(self):
if "Loop Connection" not in self.inputs:
return
val = self._get_optional_input_value("Loop Connection", int)
if val != self.num_socks and isinstance(val, int):
self.num_socks = val
def init(self, context: bpy.types.Context) -> None: def init(self, context: bpy.types.Context) -> None:
self.inputs.new("NodeSocketInt", "Loop Connection") self.inputs.new("NodeSocketInt", "Loop Connection")
@ -720,22 +757,6 @@ class LoopOutputNode(AbstractRigNodesNode):
obj = self._get_input_value(f"Value", float) obj = self._get_input_value(f"Value", float)
self.outputs[f"Value {index}"].default_value = obj self.outputs[f"Value {index}"].default_value = obj
def reset_run(self) -> None:
super().reset_run()
self.index = 0
self.iterations = self.num_socks
# 67649: The socket_value_update fn does not work when changing values in a node tree.
# Therefore using update for prototying the loop function.
# This allows to dynamically change the sockets based loop input socket amount.
def update(self):
if "Loop Connection" not in self.inputs:
return
val = self._get_optional_input_value("Loop Connection", int)
if val != self.num_socks and isinstance(val, int):
self.num_socks = val
class AbstractRigNodesEventNode(AbstractRigNodesNode): class AbstractRigNodesEventNode(AbstractRigNodesNode):
"""Node that can only exist once in a tree.""" """Node that can only exist once in a tree."""
@ -1629,6 +1650,13 @@ node_categories = [
nodeitems_utils.NodeItem("LoopOutputNode"), nodeitems_utils.NodeItem("LoopOutputNode"),
], ],
), ),
RigNodesNodeCategory(
"INPUTS",
"Inputs",
items=[
nodeitems_utils.NodeItem("ObjectInputNode"),
],
),
RigNodesNodeCategory( RigNodesNodeCategory(
"CONTROL", "CONTROL",
"Control", "Control",
@ -1693,6 +1721,7 @@ classes = (
SequenceNode, SequenceNode,
LoopInputNode, LoopInputNode,
LoopOutputNode, LoopOutputNode,
ObjectInputNode,
# Math Nodes # Math Nodes
RotateTowards, RotateTowards,
AngleFromVectors, AngleFromVectors,

View File

@ -60,7 +60,7 @@ class RigNodes_OT_prepare_tree(bpy.types.Operator):
"""Prepare node tree manually.""" """Prepare node tree manually."""
bl_idname = "rignodes.prepare_tree" bl_idname = "rignodes.prepare_tree"
bl_label = "Prepare Tree" bl_label = "Reset Tree"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod

Binary file not shown.