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)

119
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.
@ -123,10 +123,9 @@ class RigNodesNodeTree(bpy.types.NodeTree):
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:
cgtinker marked this conversation as resolved Outdated
  • Replace not X in Y with X not in Y.
  • Don't count the number of nodes unless you want to know the count. In this case and nodes will be enough to express 'list is not empty'
  • A 'list is not empty' check is likely to be faster than a list lookup, so do that first

The result:

if nodes and input_node not in nodes:
    ...
- Replace `not X in Y` with `X not in Y`. - Don't count the number of nodes unless you want to know the count. In this case `and nodes` will be enough to express 'list is not empty' - A 'list is not empty' check is likely to be faster than a list lookup, so do that first The result: ```py if nodes and input_node not in nodes: ... ```

I agree, actually rewrote that part completely. It skipped nodes when there were looping nodes within the loops.

ED: Due to the new logic I think it shouldn't be to hard to implement the points that have been out of scope. For the backwards solver that should only change how to search for innern loop nodes. But I'm not sure if I'd even like to have "setting attributes within loops" to be supported - doesn't make a lot of sense especially for nested loops.

I agree, actually rewrote that part completely. It skipped nodes when there were looping nodes within the loops. ED: Due to the new logic I think it shouldn't be to hard to implement the points that have been out of scope. For the backwards solver that should only change how to search for innern loop nodes. But I'm not sure if I'd even like to have "setting attributes within loops" to be supported - doesn't make a lot of sense especially for nested loops.
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.