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
import bpy
from typing import List
class RIGNODES_MT_snap_pie(bpy.types.Menu):
@ -60,7 +61,7 @@ def draw_nodetree_header_menu(self, context):
row.operator("rignodes.rebuild_node")
addon_keymaps = []
addon_keymaps: List[bpy.types.KeyMap] = []
classes = (RIGNODES_MT_snap_pie,)
_register, _unregister = bpy.utils.register_classes_factory(classes)

125
nodes.py
View File

@ -81,7 +81,7 @@ class RigNodesNodeTree(bpy.types.NodeTree):
# Set iteration count foreach (nested) loop.
for output_node in output_nodes:
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:
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:
continue
@ -93,14 +93,14 @@ class RigNodesNodeTree(bpy.types.NodeTree):
depsgraph: bpy.types.Depsgraph,
to_visit: deque["AbstractRigNodesNode"],
depth: int = 1,
) -> deque['AbstractRigNodesNode']:
"""Run one iteration of a loop.
) -> deque["AbstractRigNodesNode"]:
"""Runs (nested) loops.
: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")
input_node = node
output_node = node._get_connection_node()
visited: Set["AbstractRigNodesNode"] = set()
@ -111,7 +111,7 @@ class RigNodesNodeTree(bpy.types.NodeTree):
if isinstance(node, LoopInputNode) and node != input_node:
# Find the nested loop.
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)
# 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.
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
if node in visited:
# 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'.
continue
@ -139,7 +138,6 @@ class RigNodesNodeTree(bpy.types.NodeTree):
return to_visit
def _run_from_node(
self, depsgraph: bpy.types.Depsgraph, start_node: "AbstractRigNodesNode"
) -> None:
@ -160,11 +158,22 @@ class RigNodesNodeTree(bpy.types.NodeTree):
continue
if isinstance(node, LoopInputNode):
# Run all iterations if the found loop (recursive if nested).
# nested_output_node = node._get_connection_node()
# nested_loop = deque(nested_output_node._get_nodes_between(node))
# nested_loop.append(nested_output_node)
self._run_loop(node, depsgraph, to_visit)
if node in visited:
to_visit.popleft()
continue
# 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
# 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):
return self.outputs
def _get_nodes_between(
self, to_node: "AbstractRigNodesNode"
def _get_loop_nodes(
self: "LoopOutputNode", input_node: "LoopInputNode"
) -> 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"))
if not to_node in nodes and len(nodes) > 0:
if not input_node in nodes and len(nodes) > 0:
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
def exec_order_prerequisites(self) -> Iterable["AbstractRigNodesNode"]:
@ -521,6 +530,20 @@ class AbstractRigNodesNode(bpy.types.Node):
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:
self.recreate(context)
@ -559,13 +582,6 @@ class LoopInputNode(AbstractRigNodesNode):
super().draw_buttons(context, layout)
layout.prop(self, "dtype")
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":
# The 'Loop Connection' should be connected to a 'Loop Input Node',
@ -582,6 +598,12 @@ class LoopInputNode(AbstractRigNodesNode):
return connected_socket.node
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):
super().reset_run()
self.index = 0
@ -657,7 +679,10 @@ class LoopOutputNode(AbstractRigNodesNode):
) -> None:
super().draw_buttons(context, layout)
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":
# The 'Loop Connection' should be connected to a 'Loop Input Node',
@ -672,9 +697,21 @@ class LoopOutputNode(AbstractRigNodesNode):
return connected_socket.node
raise RuntimeError("Loop Input Node is required to execute a loop.")
@AbstractRigNodesNode._inputs.getter # type: ignore
def _inputs(self):
return self.inputs[1:]
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
def init(self, context: bpy.types.Context) -> None:
self.inputs.new("NodeSocketInt", "Loop Connection")
@ -720,22 +757,6 @@ class LoopOutputNode(AbstractRigNodesNode):
obj = self._get_input_value(f"Value", float)
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):
"""Node that can only exist once in a tree."""
@ -1629,6 +1650,13 @@ node_categories = [
nodeitems_utils.NodeItem("LoopOutputNode"),
],
),
RigNodesNodeCategory(
"INPUTS",
"Inputs",
items=[
nodeitems_utils.NodeItem("ObjectInputNode"),
],
),
RigNodesNodeCategory(
"CONTROL",
"Control",
@ -1693,6 +1721,7 @@ classes = (
SequenceNode,
LoopInputNode,
LoopOutputNode,
ObjectInputNode,
# Math Nodes
RotateTowards,
AngleFromVectors,

View File

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

Binary file not shown.