nodebpy 0.7.2__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {nodebpy-0.7.2 → nodebpy-0.8.0}/PKG-INFO +24 -22
  2. {nodebpy-0.7.2 → nodebpy-0.8.0}/README.md +20 -20
  3. {nodebpy-0.7.2 → nodebpy-0.8.0}/pyproject.toml +6 -4
  4. nodebpy-0.8.0/src/nodebpy/arrange.py +336 -0
  5. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/builder.py +25 -10
  6. nodebpy-0.7.2/src/nodebpy/arrange.py +0 -365
  7. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/__init__.py +0 -0
  8. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  9. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  10. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  11. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  12. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  13. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  14. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  15. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  16. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  17. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  18. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  19. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  20. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/__init__.py +0 -0
  21. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  22. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  23. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  24. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  25. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  26. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  27. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  28. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  29. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  30. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  31. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  32. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/texture.py +0 -0
  33. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  34. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  35. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  36. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  37. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  38. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  39. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  40. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  41. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  42. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  43. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  44. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  45. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  46. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  47. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  48. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  49. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/color.py +0 -0
  50. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  51. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  52. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/group.py +0 -0
  53. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/input.py +0 -0
  54. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/interface.py +0 -0
  55. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  56. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/output.py +0 -0
  57. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/script.py +0 -0
  58. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  59. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  60. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  61. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/nodes/shader/zone.py +0 -0
  62. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/screenshot.py +0 -0
  63. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/sockets.py +0 -0
  64. {nodebpy-0.7.2 → nodebpy-0.8.0}/src/nodebpy/types.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: Build nodes trees in Blender more elegantly with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
7
- Requires-Dist: networkx>=3.6.1
8
7
  Requires-Dist: bpy>=5.0.1 ; extra == 'bpy'
9
8
  Requires-Dist: fake-bpy-module>=20260113 ; extra == 'dev'
10
9
  Requires-Dist: jsondiff>=2.2.1 ; extra == 'dev'
10
+ Requires-Dist: networkx>=3.6.1 ; extra == 'dev'
11
11
  Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
12
12
  Requires-Dist: pytest-cov>=7.0.0 ; extra == 'dev'
13
13
  Requires-Dist: quarto-cli>=1.8.26 ; extra == 'dev'
@@ -16,10 +16,12 @@ Requires-Dist: ruff>=0.14.11 ; extra == 'dev'
16
16
  Requires-Dist: syrupy>=5.0.0 ; extra == 'dev'
17
17
  Requires-Dist: tree-clipper>=0.1.1 ; extra == 'dev'
18
18
  Requires-Dist: ipython>=8.0.0 ; extra == 'jupyter'
19
+ Requires-Dist: networkx>=3.6.1 ; extra == 'networkx'
19
20
  Requires-Python: >=3.11
20
21
  Provides-Extra: bpy
21
22
  Provides-Extra: dev
22
23
  Provides-Extra: jupyter
24
+ Provides-Extra: networkx
23
25
  Description-Content-Type: text/markdown
24
26
 
25
27
 
@@ -57,9 +59,9 @@ In `nodebpy` we use the `>>` operator to link from one node or socket into anoth
57
59
  This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i_*` and `o_*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
58
60
 
59
61
  ```py
60
- n.Vector() >> n.SetPosition().i_offset
61
- n.Vector() >> n.SetPosition(offset=...)
62
- n.SetPosition(offset=n.Vector())
62
+ g.Vector() >> g.SetPosition().i_offset
63
+ g.Vector() >> g.SetPosition(offset=...)
64
+ g.SetPosition(offset=g.Vector())
63
65
  ```
64
66
 
65
67
  The `>>` operator will always look for the _most_ compatible sockets first (matching data types) before looking for other compatible but not identical socket data types to link.
@@ -76,7 +78,7 @@ Entering the `tree.inputs` and `tree.outputs` contexts will let you add new inte
76
78
 
77
79
  ```py
78
80
  with TreeBuilder("MyTree") as tree:
79
- points = n.Points(position=n.RandomValue.vector(min=-1))
81
+ points = g.Points(position=g.RandomValue.vector(min=-1))
80
82
  with tree.outputs:
81
83
  points >> s.SocketGeometry("New Points")
82
84
  ```
@@ -86,7 +88,7 @@ with TreeBuilder("MyTree") as tree:
86
88
  The node tree below creates a integer input and geometry output to the node group. We create a `rotation` variable that can be used later on as an argument, then construct a longer chain of nodes being created and linked together. The nodes are added and linked as each node is instantiated. After we exit the tree context, the nodes are automatically arranged.
87
89
 
88
90
  ``` python
89
- from nodebpy import TreeBuilder, nodes as n, sockets as s
91
+ from nodebpy import TreeBuilder, geometry as g, sockets as s
90
92
 
91
93
  with TreeBuilder("AnotherTree", collapse=True) as tree:
92
94
  with tree.inputs:
@@ -95,21 +97,21 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
95
97
  instances = s.SocketGeometry("Instances")
96
98
 
97
99
  rotation = (
98
- n.RandomValue.vector(min=-1, seed=2)
99
- >> n.AlignRotationToVector()
100
- >> n.RotateRotation(rotate_by=n.AxisAngleToRotation(angle=0.3))
100
+ g.RandomValue.vector(min=-1, seed=2)
101
+ >> g.AlignRotationToVector()
102
+ >> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
101
103
  )
102
104
 
103
105
  _ = (
104
106
  count
105
- >> n.Points(position=n.RandomValue.vector(min=-1))
106
- >> n.InstanceOnPoints(instance=n.Cube(), rotation=rotation)
107
- >> n.SetPosition(
108
- position=n.Position() * 2.0 + (0, 0.2, 0.3),
107
+ >> g.Points(position=g.RandomValue.vector(min=-1))
108
+ >> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
109
+ >> g.SetPosition(
110
+ position=g.Position() * 2.0 + (0, 0.2, 0.3),
109
111
  offset=(0, 0, 0.1),
110
112
  )
111
- >> n.RealizeInstances()
112
- >> n.InstanceOnPoints(n.Cube(), instance=...)
113
+ >> g.RealizeInstances()
114
+ >> g.InstanceOnPoints(g.Cube(), instance=...)
113
115
  >> instances
114
116
  )
115
117
  ```
@@ -126,16 +128,16 @@ The basic math operators also automatically add relevant nodes with their operat
126
128
 
127
129
  ```py
128
130
  # operation is exposed as a property
129
- math = n.Math(1.0, 2.0, operation='ADD')
131
+ math = g.Math(1.0, 2.0, operation='ADD')
130
132
  math.operation = "SUBTRACT"
131
133
 
132
134
  # operation can be chose as a class method
133
- math = n.Math.subtract(1.0, 2.0)
134
- math = n.Math.add(1.0, 2.0)
135
+ math = g.Math.subtract(1.0, 2.0)
136
+ math = g.Math.add(1.0, 2.0)
135
137
 
136
- # these are equivalent, the n.Math.multiply is automatically added
137
- n.Value(1.0) * 2
138
- n.Math.multiply(n.Value(1.0), 2.0)
138
+ # these are equivalent, the g.Math.multiply is automatically added
139
+ g.Value(1.0) * 2
140
+ g.Math.multiply(g.Value(1.0), 2.0)
139
141
  ```
140
142
 
141
143
  # Design Considerations
@@ -33,9 +33,9 @@ In `nodebpy` we use the `>>` operator to link from one node or socket into anoth
33
33
  This should feel and behave much like the <kbd>Alt</kbd> + <kbd>Right Click</kbd> drag between nodes in [Node Wrangler](https://docs.blender.org/manual/en/latest/addons/node/node_wrangler.html). It will use some smart logic to match the most compatible sockets between the nodes, but if you ever want to be explicit you do so. The input and output sockets of a node are accessible as properties via the `i_*` and `o_*` prefixes, or you can use the `...` placeholder to specify the particular input to be user, or pass in the previous node as a named argument.
34
34
 
35
35
  ```py
36
- n.Vector() >> n.SetPosition().i_offset
37
- n.Vector() >> n.SetPosition(offset=...)
38
- n.SetPosition(offset=n.Vector())
36
+ g.Vector() >> g.SetPosition().i_offset
37
+ g.Vector() >> g.SetPosition(offset=...)
38
+ g.SetPosition(offset=g.Vector())
39
39
  ```
40
40
 
41
41
  The `>>` operator will always look for the _most_ compatible sockets first (matching data types) before looking for other compatible but not identical socket data types to link.
@@ -52,7 +52,7 @@ Entering the `tree.inputs` and `tree.outputs` contexts will let you add new inte
52
52
 
53
53
  ```py
54
54
  with TreeBuilder("MyTree") as tree:
55
- points = n.Points(position=n.RandomValue.vector(min=-1))
55
+ points = g.Points(position=g.RandomValue.vector(min=-1))
56
56
  with tree.outputs:
57
57
  points >> s.SocketGeometry("New Points")
58
58
  ```
@@ -62,7 +62,7 @@ with TreeBuilder("MyTree") as tree:
62
62
  The node tree below creates a integer input and geometry output to the node group. We create a `rotation` variable that can be used later on as an argument, then construct a longer chain of nodes being created and linked together. The nodes are added and linked as each node is instantiated. After we exit the tree context, the nodes are automatically arranged.
63
63
 
64
64
  ``` python
65
- from nodebpy import TreeBuilder, nodes as n, sockets as s
65
+ from nodebpy import TreeBuilder, geometry as g, sockets as s
66
66
 
67
67
  with TreeBuilder("AnotherTree", collapse=True) as tree:
68
68
  with tree.inputs:
@@ -71,21 +71,21 @@ with TreeBuilder("AnotherTree", collapse=True) as tree:
71
71
  instances = s.SocketGeometry("Instances")
72
72
 
73
73
  rotation = (
74
- n.RandomValue.vector(min=-1, seed=2)
75
- >> n.AlignRotationToVector()
76
- >> n.RotateRotation(rotate_by=n.AxisAngleToRotation(angle=0.3))
74
+ g.RandomValue.vector(min=-1, seed=2)
75
+ >> g.AlignRotationToVector()
76
+ >> g.RotateRotation(rotate_by=g.AxisAngleToRotation(angle=0.3))
77
77
  )
78
78
 
79
79
  _ = (
80
80
  count
81
- >> n.Points(position=n.RandomValue.vector(min=-1))
82
- >> n.InstanceOnPoints(instance=n.Cube(), rotation=rotation)
83
- >> n.SetPosition(
84
- position=n.Position() * 2.0 + (0, 0.2, 0.3),
81
+ >> g.Points(position=g.RandomValue.vector(min=-1))
82
+ >> g.InstanceOnPoints(instance=g.Cube(), rotation=rotation)
83
+ >> g.SetPosition(
84
+ position=g.Position() * 2.0 + (0, 0.2, 0.3),
85
85
  offset=(0, 0, 0.1),
86
86
  )
87
- >> n.RealizeInstances()
88
- >> n.InstanceOnPoints(n.Cube(), instance=...)
87
+ >> g.RealizeInstances()
88
+ >> g.InstanceOnPoints(g.Cube(), instance=...)
89
89
  >> instances
90
90
  )
91
91
  ```
@@ -102,16 +102,16 @@ The basic math operators also automatically add relevant nodes with their operat
102
102
 
103
103
  ```py
104
104
  # operation is exposed as a property
105
- math = n.Math(1.0, 2.0, operation='ADD')
105
+ math = g.Math(1.0, 2.0, operation='ADD')
106
106
  math.operation = "SUBTRACT"
107
107
 
108
108
  # operation can be chose as a class method
109
- math = n.Math.subtract(1.0, 2.0)
110
- math = n.Math.add(1.0, 2.0)
109
+ math = g.Math.subtract(1.0, 2.0)
110
+ math = g.Math.add(1.0, 2.0)
111
111
 
112
- # these are equivalent, the n.Math.multiply is automatically added
113
- n.Value(1.0) * 2
114
- n.Math.multiply(n.Value(1.0), 2.0)
112
+ # these are equivalent, the g.Math.multiply is automatically added
113
+ g.Value(1.0) * 2
114
+ g.Math.multiply(g.Value(1.0), 2.0)
115
115
  ```
116
116
 
117
117
  # Design Considerations
@@ -1,20 +1,21 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "0.7.2"
3
+ version = "0.8.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Brady Johnston", email = "brady.johnston@me.com" }
8
8
  ]
9
9
  requires-python = ">=3.11"
10
- dependencies = [
11
- "networkx>=3.6.1",
12
- ]
10
+ dependencies = []
13
11
 
14
12
  [project.scripts]
15
13
  nodebpy = "nodebpy:main"
16
14
 
17
15
  [project.optional-dependencies]
16
+ networkx = [
17
+ "networkx>=3.6.1",
18
+ ]
18
19
  bpy = [
19
20
  "bpy>=5.0.1",
20
21
  ]
@@ -24,6 +25,7 @@ jupyter = [
24
25
  dev = [
25
26
  "fake-bpy-module>=20260113",
26
27
  "jsondiff>=2.2.1",
28
+ "networkx>=3.6.1",
27
29
  "pytest>=9.0.2",
28
30
  "pytest-cov>=7.0.0",
29
31
  "quarto-cli>=1.8.26",
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, deque
4
+
5
+ import bpy
6
+
7
+
8
+ def _is_layoutable(node: bpy.types.Node) -> bool:
9
+ """Check if a node should be included in column-based layout.
10
+
11
+ Frame nodes are containers and reroute nodes are tiny routing helpers;
12
+ neither should occupy a full column slot.
13
+ """
14
+ return node.bl_idname not in ("NodeFrame", "NodeReroute")
15
+
16
+
17
+ def build_dependency_graph(
18
+ tree: bpy.types.NodeTree,
19
+ ) -> tuple[dict[bpy.types.Node, set[bpy.types.Node]], Counter]:
20
+ """Build a graph of node dependencies and count input connections.
21
+
22
+ Only layoutable nodes (excluding frames and reroutes) are included.
23
+ """
24
+ layoutable = {n for n in tree.nodes if _is_layoutable(n)}
25
+ dependency_graph: dict[bpy.types.Node, set[bpy.types.Node]] = {
26
+ node: set() for node in layoutable
27
+ }
28
+ socket_input_connection_count: Counter = Counter()
29
+
30
+ for link in tree.links:
31
+ if link.from_node in layoutable and link.to_node in layoutable:
32
+ dependency_graph[link.from_node].add(link.to_node)
33
+ socket_input_connection_count[link.to_socket] += 1
34
+
35
+ return dependency_graph, socket_input_connection_count
36
+
37
+
38
+ def topological_sort(
39
+ dependency_graph: dict[bpy.types.Node, set[bpy.types.Node]],
40
+ ) -> list[bpy.types.Node]:
41
+ """Sort nodes in topological (dependency) order using Kahn's algorithm."""
42
+ incoming = {node: 0 for node in dependency_graph}
43
+ for dependents in dependency_graph.values():
44
+ for target in dependents:
45
+ incoming[target] += 1
46
+
47
+ queue = deque(node for node, count in incoming.items() if count == 0)
48
+ result: list[bpy.types.Node] = []
49
+
50
+ while queue:
51
+ current = queue.popleft()
52
+ result.append(current)
53
+ for dependent in dependency_graph[current]:
54
+ incoming[dependent] -= 1
55
+ if incoming[dependent] == 0:
56
+ queue.append(dependent)
57
+
58
+ return result
59
+
60
+
61
+ def organize_into_columns(
62
+ nodes_in_order: list[bpy.types.Node],
63
+ dependency_graph: dict[bpy.types.Node, set[bpy.types.Node]],
64
+ ) -> list[list[bpy.types.Node]]:
65
+ """Assign each node to a column based on its furthest dependent."""
66
+ columns: list[list[bpy.types.Node]] = []
67
+ column_of: dict[bpy.types.Node, int] = {}
68
+
69
+ for node in reversed(nodes_in_order):
70
+ col = (
71
+ max(
72
+ (column_of[dep] for dep in dependency_graph[node]),
73
+ default=-1,
74
+ )
75
+ + 1
76
+ )
77
+ column_of[node] = col
78
+
79
+ if col == len(columns):
80
+ columns.append([node])
81
+ else:
82
+ columns[col].append(node)
83
+
84
+ # reverse so flow goes left-to-right
85
+ return list(reversed(columns))
86
+
87
+
88
+ def calculate_node_dimensions(
89
+ node: bpy.types.Node,
90
+ socket_input_connection_count: Counter,
91
+ interface_scale: float,
92
+ ) -> tuple[float, float]:
93
+ """Calculate the visual dimensions of a node.
94
+
95
+ When a node is collapsed (``node.hide is True``) only linked sockets
96
+ contribute to the height, and header / property / vector-expansion rows
97
+ are omitted.
98
+ """
99
+ HEADER = 20
100
+ SOCKET = 32
101
+ HIDDEN_SOCKET = 14
102
+ HIDDEN_HEADER = 30
103
+
104
+ if node.hide:
105
+ linked_inputs = sum(1 for s in node.inputs if s.enabled and s.is_linked)
106
+ linked_outputs = sum(1 for s in node.outputs if s.enabled and s.is_linked)
107
+ visible = max(linked_inputs, linked_outputs, 1)
108
+ height = (HIDDEN_HEADER + visible * HIDDEN_SOCKET) * interface_scale
109
+ return node.width, height
110
+ PROPERTY_ROW = 28
111
+ VECTOR_EXPANDED = 84
112
+
113
+ enabled_inputs = sum(1 for s in node.inputs if s.enabled)
114
+ enabled_outputs = sum(1 for s in node.outputs if s.enabled)
115
+
116
+ # count properties specific to this node type (not inherited)
117
+ inherited_ids = {
118
+ prop.identifier
119
+ for base in type(node).__bases__
120
+ for prop in base.bl_rna.properties
121
+ }
122
+ node_property_count = sum(
123
+ 1 for prop in node.bl_rna.properties if prop.identifier not in inherited_ids
124
+ )
125
+
126
+ # count vector inputs that need expanded UI widgets (not connected)
127
+ unconnected_vectors = sum(
128
+ 1
129
+ for s in node.inputs
130
+ if s.enabled and s.type == "VECTOR" and socket_input_connection_count[s] == 0
131
+ )
132
+
133
+ height = (
134
+ HEADER
135
+ + enabled_outputs * SOCKET
136
+ + node_property_count * PROPERTY_ROW
137
+ + enabled_inputs * SOCKET
138
+ + unconnected_vectors * VECTOR_EXPANDED
139
+ ) * interface_scale
140
+
141
+ return node.width, height
142
+
143
+
144
+ def _socket_index(socket: bpy.types.NodeSocket) -> int:
145
+ """Return the index of a socket among its node's enabled sockets."""
146
+ collection = socket.node.inputs if not socket.is_output else socket.node.outputs
147
+ idx = 0
148
+ for s in collection:
149
+ if s == socket:
150
+ return idx
151
+ if s.enabled:
152
+ idx += 1
153
+ return idx
154
+
155
+
156
+ def _reduce_crossings(
157
+ columns: list[list[bpy.types.Node]],
158
+ tree: bpy.types.NodeTree,
159
+ passes: int = 4,
160
+ ) -> None:
161
+ """Reorder nodes within columns to reduce edge crossings.
162
+
163
+ Uses the barycenter heuristic with socket-level precision: for each node
164
+ compute its weight from the position of the sockets it connects to in the
165
+ adjacent column, then sort by that weight. This correctly distinguishes
166
+ nodes that connect to different sockets on the same target.
167
+
168
+ Alternating forward and backward sweeps iteratively improve the ordering.
169
+ """
170
+ if len(columns) < 2:
171
+ return
172
+
173
+ layoutable = {n for col in columns for n in col}
174
+ col_of = {n: ci for ci, col in enumerate(columns) for n in col}
175
+
176
+ # Pre-compute per-node link weights towards each adjacent column direction.
177
+ # For a forward sweep (fixing col i, sorting col i+1), a node in col i+1
178
+ # cares about its connections INTO col i. The weight of each connection is
179
+ # the position of the *node* in the fixed column plus a fractional offset
180
+ # derived from the socket index, so that multiple links to the same node
181
+ # produce distinct, correctly ordered weights.
182
+ #
183
+ # We store raw (neighbour_node, socket_fraction) pairs per node per
184
+ # direction and resolve them during each sweep once column order is known.
185
+
186
+ # link records: for each layoutable node, collect tuples of
187
+ # (neighbour_node, socket_fraction)
188
+ # keyed by which side the neighbour is on (left or right).
189
+ left_links: dict[bpy.types.Node, list[tuple[bpy.types.Node, float]]] = {
190
+ n: [] for n in layoutable
191
+ }
192
+ right_links: dict[bpy.types.Node, list[tuple[bpy.types.Node, float]]] = {
193
+ n: [] for n in layoutable
194
+ }
195
+
196
+ for link in tree.links:
197
+ src, dst = link.from_node, link.to_node
198
+ if src not in layoutable or dst not in layoutable:
199
+ continue
200
+ src_col, dst_col = col_of[src], col_of[dst]
201
+ if src_col >= dst_col:
202
+ continue # only consider forward edges
203
+
204
+ # Weight based on socket position on the neighbour node.
205
+ # For a node in the right column looking left: the relevant socket is
206
+ # on the source node (output side).
207
+ # For a node in the left column looking right: the relevant socket is
208
+ # on the target node (input side).
209
+ out_count = max(1, sum(1 for s in src.outputs if s.enabled))
210
+ in_count = max(1, sum(1 for s in dst.inputs if s.enabled))
211
+ src_frac = _socket_index(link.from_socket) / out_count
212
+ dst_frac = _socket_index(link.to_socket) / in_count
213
+
214
+ # dst looks left towards src: weight by src's output socket position
215
+ left_links[dst].append((src, src_frac))
216
+ # src looks right towards dst: weight by dst's input socket position
217
+ right_links[src].append((dst, dst_frac))
218
+
219
+ for iteration in range(passes):
220
+ if iteration % 2 == 0:
221
+ # forward sweep: fix column i, sort column i+1
222
+ col_range = range(1, len(columns))
223
+ else:
224
+ # backward sweep: fix column i, sort column i-1
225
+ col_range = range(len(columns) - 2, -1, -1)
226
+
227
+ for ci in col_range:
228
+ if iteration % 2 == 0:
229
+ fixed_col = columns[ci - 1]
230
+ links_map = left_links
231
+ else:
232
+ fixed_col = columns[ci + 1]
233
+ links_map = right_links
234
+
235
+ pos_in_fixed = {node: idx for idx, node in enumerate(fixed_col)}
236
+ original_pos = {node: float(idx) for idx, node in enumerate(columns[ci])}
237
+
238
+ barycenters: dict[bpy.types.Node, float] = {}
239
+ for node in columns[ci]:
240
+ weights = [
241
+ pos_in_fixed[nb] + frac
242
+ for nb, frac in links_map[node]
243
+ if nb in pos_in_fixed
244
+ ]
245
+ if weights:
246
+ barycenters[node] = sum(weights) / len(weights)
247
+ else:
248
+ barycenters[node] = original_pos[node]
249
+
250
+ columns[ci].sort(key=lambda n: barycenters[n])
251
+
252
+
253
+ def position_nodes_in_columns(
254
+ columns: list[list[bpy.types.Node]],
255
+ connection_counts: Counter,
256
+ spacing: tuple[float, float] = (50, 25),
257
+ ) -> None:
258
+ """Position nodes column-by-column with the given spacing.
259
+
260
+ Consecutive collapsed nodes are stacked tightly (with minimal gap) to
261
+ keep related math/converter chains visually grouped together.
262
+ """
263
+ COLLAPSED_GAP = 4
264
+
265
+ x = 0.0
266
+ for column in columns:
267
+ col_width = 0.0
268
+ y = 0.0
269
+ prev_hidden = False
270
+
271
+ for node in column:
272
+ node.update()
273
+
274
+ width, height = calculate_node_dimensions(node, connection_counts, 1.0)
275
+
276
+ if width > col_width:
277
+ col_width = width
278
+
279
+ node.location = (x, y)
280
+
281
+ # use tight spacing between consecutive collapsed nodes
282
+ if node.hide and prev_hidden:
283
+ y -= height + COLLAPSED_GAP
284
+ else:
285
+ y -= height + spacing[1]
286
+
287
+ prev_hidden = node.hide
288
+
289
+ x += col_width + spacing[0]
290
+
291
+
292
+ def position_reroutes(tree: bpy.types.NodeTree) -> None:
293
+ """Place reroute nodes midway between their source and target."""
294
+ for node in tree.nodes:
295
+ if node.bl_idname != "NodeReroute":
296
+ continue
297
+
298
+ sources: list[bpy.types.Node] = []
299
+ targets: list[bpy.types.Node] = []
300
+ for link in tree.links:
301
+ if link.to_node == node:
302
+ sources.append(link.from_node)
303
+ if link.from_node == node:
304
+ targets.append(link.to_node)
305
+
306
+ neighbours = sources + targets
307
+ if not neighbours:
308
+ continue
309
+
310
+ avg_x = sum(n.location.x for n in neighbours) / len(neighbours)
311
+ avg_y = sum(n.location.y for n in neighbours) / len(neighbours)
312
+ node.location = (avg_x, avg_y)
313
+
314
+
315
+ def arrange_tree(
316
+ tree: bpy.types.NodeTree,
317
+ spacing: tuple[float, float] = (50, 25),
318
+ ) -> None:
319
+ """Arrange nodes in a node tree based on their dependencies.
320
+
321
+ Organises layoutable nodes into columns from left to right and positions
322
+ reroute nodes between their neighbours. Frame nodes are left untouched.
323
+ """
324
+ if not tree.nodes:
325
+ return
326
+
327
+ dependency_graph, connection_counts = build_dependency_graph(tree)
328
+
329
+ if not dependency_graph:
330
+ return
331
+
332
+ nodes_in_order = topological_sort(dependency_graph)
333
+ columns = organize_into_columns(nodes_in_order, dependency_graph)
334
+ _reduce_crossings(columns, tree)
335
+ position_nodes_in_columns(columns, connection_counts, spacing)
336
+ position_reroutes(tree)
@@ -18,7 +18,7 @@ from bpy.types import (
18
18
  ShaderNodeTree,
19
19
  )
20
20
 
21
- from .lib.nodearrange import arrange
21
+ from .arrange import arrange_tree
22
22
  from .types import (
23
23
  LINKABLE,
24
24
  SOCKET_COMPATIBILITY,
@@ -32,8 +32,6 @@ from .types import (
32
32
  _SocketShapeStructureType,
33
33
  )
34
34
 
35
- # from .arrange import arrange_tree
36
-
37
35
  GEO_NODE_NAMES = (
38
36
  f"GeometryNode{name}"
39
37
  for name in (
@@ -169,7 +167,7 @@ class TreeBuilder:
169
167
  "GeometryNodeTree", "ShaderNodeTree", "CompositorNodeTree"
170
168
  ] = "GeometryNodeTree",
171
169
  collapse: bool = False,
172
- arrange: bool = True,
170
+ arrange: Literal["sugiyama", "simple"] | None = "sugiyama",
173
171
  fake_user: bool = False,
174
172
  ):
175
173
  if isinstance(tree, str):
@@ -191,7 +189,7 @@ class TreeBuilder:
191
189
  name: GeometryNodeTree | str = "Geometry Nodes",
192
190
  *,
193
191
  collapse: bool = False,
194
- arrange: bool = True,
192
+ arrange: Literal["sugiyama", "simple"] | None = "sugiyama",
195
193
  fake_user: bool = False,
196
194
  ) -> "TreeBuilder":
197
195
  """Create a geometry node tree."""
@@ -209,7 +207,7 @@ class TreeBuilder:
209
207
  name: ShaderNodeTree | str = "Shader Nodes",
210
208
  *,
211
209
  collapse: bool = False,
212
- arrange: bool = True,
210
+ arrange: Literal["sugiyama", "simple"] | None = "sugiyama",
213
211
  fake_user: bool = False,
214
212
  ) -> "TreeBuilder":
215
213
  """Create a shader node tree."""
@@ -227,7 +225,7 @@ class TreeBuilder:
227
225
  name: CompositorNodeTree | str = "Compositor Nodes",
228
226
  *,
229
227
  collapse: bool = False,
230
- arrange: bool = True,
228
+ arrange: Literal["sugiyama", "simple"] | None = "sugiyama",
231
229
  fake_user: bool = False,
232
230
  ) -> "TreeBuilder":
233
231
  """Create a compositor node tree."""
@@ -264,7 +262,7 @@ class TreeBuilder:
264
262
  return self
265
263
 
266
264
  def __exit__(self, *args):
267
- if self._arrange:
265
+ if self._arrange is not None:
268
266
  self.arrange()
269
267
  self._apply_input_defaults()
270
268
  self.deactivate_tree()
@@ -280,8 +278,25 @@ class TreeBuilder:
280
278
  return len(self.nodes)
281
279
 
282
280
  def arrange(self):
283
- arrange.sugiyama.sugiyama_layout(self.tree)
284
- arrange.sugiyama.config.reset()
281
+ if self._arrange == "sugiyama":
282
+ try:
283
+ from .lib.nodearrange import arrange as nodearrange
284
+
285
+ nodearrange.sugiyama.sugiyama_layout(self.tree)
286
+ nodearrange.sugiyama.config.reset()
287
+ except ImportError as e:
288
+ if "networkx" not in str(e):
289
+ raise
290
+ import warnings
291
+
292
+ warnings.warn(
293
+ "networkx is not installed, falling back to simple arrangement. "
294
+ "Install networkx for the Sugiyama layout: pip install nodebpy[networkx]",
295
+ stacklevel=2,
296
+ )
297
+ arrange_tree(self.tree)
298
+ elif self._arrange == "simple":
299
+ arrange_tree(self.tree)
285
300
 
286
301
  def _repr_markdown_(self) -> str | None:
287
302
  """
@@ -1,365 +0,0 @@
1
- import typing
2
- from collections import Counter, deque
3
-
4
- import bpy
5
- from mathutils import Vector
6
-
7
-
8
- def contains_geo_socket(sockets: bpy.types.NodeInputs | bpy.types.NodeOutputs) -> bool:
9
- """
10
- Check if any socket in the collection is a geometry socket
11
-
12
- Parameters
13
- ----------
14
- sockets : bpy.types.NodeInputs | bpy.types.NodeOutputs
15
- Collection of node sockets to check
16
-
17
- Returns
18
- -------
19
- bool
20
- True if any socket is a geometry socket
21
- """
22
- return any([s.bl_idname == "NodeSocketGeometry" for s in sockets])
23
-
24
-
25
- def node_has_geo_socket(node: bpy.types.GeometryNode) -> bool:
26
- """
27
- Check if a node has any geometry sockets in its inputs or outputs
28
-
29
- Parameters
30
- ----------
31
- node : bpy.types.GeometryNode
32
- The node to check
33
-
34
- Returns
35
- -------
36
- bool
37
- True if the node has at least one geometry socket
38
- """
39
- return any([contains_geo_socket(x) for x in [node.inputs, node.outputs]])
40
-
41
-
42
- def build_dependency_graph(tree: bpy.types.NodeTree) -> tuple[dict, Counter]:
43
- """
44
- Build a graph representing node dependencies and count input connections
45
-
46
- Parameters
47
- ----------
48
- tree : bpy.types.NodeTree
49
- The node tree to analyze
50
-
51
- Returns
52
- -------
53
- tuple
54
- Contains:
55
- dict
56
- Mapping of nodes to their dependent nodes
57
- Counter
58
- Count of connections for each socket
59
- """
60
- dependency_graph = {node: set() for node in tree.nodes}
61
- socket_input_connection_count = Counter()
62
-
63
- # populate the graph based on node connections
64
- for link in tree.links:
65
- dependency_graph[link.from_node].add(link.to_node)
66
- socket_input_connection_count[link.to_socket] += 1
67
-
68
- return dependency_graph, socket_input_connection_count
69
-
70
-
71
- def topological_sort(dependency_graph: dict) -> list:
72
- """
73
- Sort nodes by their dependencies using a topological sort algorithm
74
-
75
- Parameters
76
- ----------
77
- dependency_graph : dict
78
- Mapping of nodes to their dependent nodes
79
-
80
- Returns
81
- -------
82
- list
83
- Nodes sorted in topological order
84
- """
85
- # count incoming connections for each node
86
- incoming_connection_count = {node: 0 for node in dependency_graph}
87
- for source_node in dependency_graph:
88
- for target_node in dependency_graph[source_node]:
89
- incoming_connection_count[target_node] += 1
90
-
91
- # start with nodes that have no dependencies
92
- processing_queue = deque(
93
- [
94
- node
95
- for node in incoming_connection_count
96
- if incoming_connection_count[node] == 0
97
- ]
98
- )
99
- sorted_node_order = []
100
-
101
- # process nodes in breadth-first order
102
- while processing_queue:
103
- current_node = processing_queue.popleft()
104
- sorted_node_order.append(current_node)
105
-
106
- # update counts for nodes that depend on the current node
107
- for dependent_node in dependency_graph[current_node]:
108
- incoming_connection_count[dependent_node] -= 1
109
- # if all dependencies are processed, add to queue
110
- if incoming_connection_count[dependent_node] == 0:
111
- processing_queue.append(dependent_node)
112
-
113
- return sorted_node_order
114
-
115
-
116
- def organize_into_columns(nodes_in_order: list, dependency_graph: dict) -> list:
117
- """
118
- Organize nodes into columns based on their dependencies
119
-
120
- Parameters
121
- ----------
122
- nodes_in_order : list
123
- Nodes sorted in topological order
124
- dependency_graph : dict
125
- Mapping of nodes to their dependent nodes
126
-
127
- Returns
128
- -------
129
- list
130
- Columns of nodes, where each column is a list of nodes
131
- """
132
- node_columns = []
133
- node_column_assignment = {}
134
-
135
- for node in reversed(nodes_in_order):
136
- # node goes in column after its furthest dependent
137
- node_column_assignment[node] = (
138
- max(
139
- [
140
- node_column_assignment[dependent_node]
141
- for dependent_node in dependency_graph[node]
142
- ],
143
- default=-1,
144
- )
145
- + 1
146
- )
147
-
148
- # add node to its assigned column
149
- if node_column_assignment[node] == len(node_columns):
150
- node_columns.append([node])
151
- else:
152
- node_columns[node_column_assignment[node]].append(node)
153
-
154
- # reverse columns to get left-to-right flow
155
- return list(reversed(node_columns))
156
-
157
-
158
- def calculate_node_dimensions(
159
- node: bpy.types.Node, socket_input_connection_count: Counter, interface_scale: float
160
- ) -> tuple[float, float]:
161
- """
162
- Calculate the visual dimensions of a node
163
-
164
- Parameters
165
- ----------
166
- node : bpy.types.Node
167
- The node to calculate dimensions for
168
- socket_input_connection_count : Counter
169
- Counter of connections for each socket
170
- interface_scale : float
171
- UI scale factor
172
-
173
- Returns
174
- -------
175
- tuple[float, float]
176
- Width and height of the node
177
- """
178
- # height constants for different node elements
179
- node_header_height = 20
180
- node_socket_height = 28
181
- node_property_row_height = 28
182
- node_vector_input_height = 84
183
-
184
- # count enabled inputs and outputs
185
- enabled_input_count = len(
186
- list(filter(lambda input_socket: input_socket.enabled, node.inputs))
187
- )
188
- enabled_output_count = len(
189
- list(filter(lambda output_socket: output_socket.enabled, node.outputs))
190
- )
191
-
192
- # get properties specific to this node type (not inherited)
193
- inherited_property_ids = [
194
- property.identifier
195
- for base_class in type(node).__bases__
196
- for property in base_class.bl_rna.properties
197
- ]
198
-
199
- node_specific_property_count = len(
200
- [
201
- property
202
- for property in node.bl_rna.properties
203
- if property.identifier not in inherited_property_ids
204
- ]
205
- )
206
-
207
- # count vector inputs that need UI widgets (not connected)
208
- unconnected_vector_input_count = len(
209
- list(
210
- filter(
211
- lambda input_socket: (
212
- input_socket.enabled
213
- and input_socket.type == "VECTOR"
214
- and socket_input_connection_count[input_socket] == 0
215
- ),
216
- node.inputs,
217
- )
218
- )
219
- )
220
-
221
- # calculate total node height based on components
222
- total_node_height = (
223
- node_header_height
224
- + (enabled_output_count * node_socket_height)
225
- + (node_specific_property_count * node_property_row_height)
226
- + (enabled_input_count * node_socket_height)
227
- + (unconnected_vector_input_count * node_vector_input_height)
228
- ) * interface_scale
229
-
230
- return node.width, total_node_height
231
-
232
-
233
- def position_nodes_in_columns(
234
- node_columns: list,
235
- socket_input_connection_count: Counter,
236
- spacing: typing.Tuple[float, float] = (50, 25),
237
- ) -> None:
238
- """Position nodes in columns with appropriate spacing
239
-
240
- Parameters
241
- ----------
242
- node_columns : list
243
- List of columns, where each column is a list of nodes
244
- socket_input_connection_count : Counter
245
- Counter of connections for each socket
246
- spacing : tuple of float, optional
247
- Tuple of (horizontal, vertical) spacing between nodes, by default (50, 25)
248
- """
249
- interface_scale = 1.0
250
- non_geo_offset = 20 + 28 * 2 # header + 2 socket heights
251
-
252
- # position nodes column by column
253
- position_x = 0
254
- for column in node_columns:
255
- widest_node_in_column = 0
256
- position_y = 0
257
-
258
- for node in column:
259
- node.update()
260
-
261
- width, height = calculate_node_dimensions(
262
- node, socket_input_connection_count, interface_scale
263
- )
264
-
265
- # track widest node for column spacing
266
- if width > widest_node_in_column:
267
- widest_node_in_column = width
268
-
269
- # position node
270
- node.location = (position_x, position_y)
271
-
272
- # adjust position for non-geometry nodes
273
- if not node_has_geo_socket(node):
274
- node.location -= Vector((0, non_geo_offset))
275
-
276
- # move down for next node with spacing
277
- position_y -= height + spacing[1]
278
-
279
- # move right for next column with spacing
280
- position_x += widest_node_in_column + spacing[0]
281
-
282
-
283
- def position_special_nodes(
284
- tree: bpy.types.NodeTree, vertical_offset: float = 100
285
- ) -> None:
286
- """Position special nodes like Group Input and Group Output at the top
287
-
288
- Parameters
289
- ----------
290
- tree : bpy.types.NodeTree
291
- The node tree to modify
292
- vertical_offset : float, optional
293
- Vertical offset above the highest node, by default 100
294
-
295
- Returns
296
- -------
297
- None
298
- """
299
- highest_y_position = max([node.location[1] for node in tree.nodes])
300
- for special_node_name in ["Group Input", "Group Output"]:
301
- if special_node_name in tree.nodes:
302
- special_node = tree.nodes[special_node_name]
303
- special_node.location = (
304
- special_node.location[0],
305
- highest_y_position + vertical_offset,
306
- )
307
-
308
-
309
- def cleanup_orphaned_nodes(
310
- tree: bpy.types.NodeTree, max_iter: int = 100, add_group_input: bool = True
311
- ) -> None:
312
- """Remove nodes that are not connected to anything"""
313
- to_remove = []
314
- for _ in range(max_iter):
315
- for node in tree.nodes:
316
- if len(node.outputs) == 0:
317
- continue
318
- if not any([s.is_linked for s in node.outputs]):
319
- to_remove.append(node)
320
-
321
- if len(to_remove) == 0:
322
- break
323
-
324
- for node in to_remove:
325
- tree.nodes.remove(node)
326
-
327
- to_remove = []
328
-
329
- if add_group_input and "Group Input" not in tree.nodes:
330
- n_input = tree.nodes.new("NodeGroupInput")
331
- if "Join Geometry" in tree.nodes:
332
- tree.links.new(n_input.outputs[0], tree.nodes["Join Geometry"].inputs[0])
333
-
334
-
335
- def arrange_tree(
336
- tree: bpy.types.NodeTree,
337
- spacing: typing.Tuple[float, float] = (50, 25),
338
- add_group_input: bool = True,
339
- ) -> None:
340
- """Arrange nodes in a node tree based on their dependencies
341
-
342
- Parameters
343
- ----------
344
- tree : bpy.types.GeometryNodeTree
345
- The node tree to arrange
346
- spacing : tuple of float
347
- Tuple of (horizontal, vertical) spacing between nodes
348
-
349
- Returns
350
- -------
351
- None
352
- This function modifies the node tree in place
353
-
354
- Notes
355
- -----
356
- This function organizes nodes into columns and positions them with appropriate spacing.
357
- Nodes are arranged from left to right based on their dependencies, with special
358
- handling for geometry nodes and group input/output nodes.
359
- """
360
- cleanup_orphaned_nodes(tree, add_group_input=add_group_input)
361
- dependency_graph, socket_input_connection_count = build_dependency_graph(tree)
362
- nodes_in_dependency_order = topological_sort(dependency_graph)
363
- node_columns = organize_into_columns(nodes_in_dependency_order, dependency_graph)
364
- position_nodes_in_columns(node_columns, socket_input_connection_count, spacing)
365
- position_special_nodes(tree)
File without changes
File without changes
File without changes