Loop Nodes #4
3
gui.py
3
gui.py
@ -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
121
nodes.py
@ -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
|
|||||||
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
Sybren A. Stüvel
commented
Formatting: when writing multi-line docstrings, the closing 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))
Denys Hsu
commented
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:
|
||||||
cgtinker marked this conversation as resolved
Outdated
Sybren A. Stüvel
commented
The result:
- 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:
...
```
Denys Hsu
commented
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,
|
||||||
|
@ -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.
Loading…
Reference in New Issue
Block a user
There is no need to construct a list here, just loop over the generator returned by
output_node._get_loop_nodes()